Cross-Instance Mirror

Move a subtree of MeshNodes from one running MeshWeaver instance to another — no ZIP exports, no file uploads, no per-node back-and-forth. The most common use is pushing from local dev to prod so content you authored in memex-local appears at https://memex.meshweaver.cloud in a single command.

⚠️ Status: NOT WIRED END-TO-END. The mesh hub has no handler for MirrorRequest — verified: nothing in src/ or memex/ registers one, and the AddMirrorHandler that several code comments point at does not exist. The contract (MirrorRequest/MirrorResult in src/MeshWeaver.Mesh.Contract/Services/IMirrorOperations.cs), the mirror MCP tool, the POST /api/mesh/mirror endpoint, the import-dialog caller, and the HTTP transport are all in place, but a posted MirrorRequest gets no response — the tool falls into its error branch and the REST endpoint answers "No response from mirror handler". Everything below the "What it does" heading is the intended contract, not observed behaviour.

What it does

Local Dev localhost:7122 mirror (push) mirror (pull) Prod Portal memex.meshweaver.cloud Node import (upsert) ApiToken auth Push — outbound HTTPS Pull — outbound HTTPS (local fetches prod) Both operations run server-side; Claude Code makes one MCP tool call regardless of subtree size.

Push (mirror direction=push) and Pull (mirror direction=pull) both initiate outbound HTTPS from the local instance — no tunnel needed when targeting a public prod URL.

mirror is one MCP tool available on every MeshWeaver instance, with a direction of push (local → remote) or pull (remote → local). It executes entirely server-side: a 10 000-node migration is one MCP tool call from Claude Code's perspective, not 10 000.

Remote profiles — keep the token out of the model context

The preferred way to authenticate is a named remote profile in the host configuration:

"Mirror": {
  "Remotes": {
    "prod": { "BaseUrl": "https://memex.meshweaver.cloud", "Token": "mw_…" }
  }
}

Then the tool call is just mirror direction=push remote="prod" sourcePath="rbuergi/Story" — the ApiToken never travels through the model context, transcripts, or logs. Passing a base URL as remote also resolves the token from a profile with a matching BaseUrl. Supplying remoteToken inline remains available as an ad-hoc fallback, but is discouraged: tool arguments are visible to the model and may be persisted in conversation transcripts.

Under the hood (intended — the handler in step 2 is the missing piece):

  1. The MCP tool resolves the remote profile and posts one MirrorRequest at the mesh hub (hub.Observe<MirrorResult>).
  2. The mesh-hub handler reads every node under sourcePath and, per node, calls the destination's MCP surface (create / update) through McpRemoteMeshClient, authenticating with the destination's ApiToken.
  3. It returns a MirrorResult with the fields status, direction, sourcePath, targetPath, nodesImported, nodesSkipped, nodesRemoved, partitionsImported, and elapsedMs, which the tool serialises back.

Network direction matrix

The tool initiates outbound HTTPS from the side it runs on, in both directions. The rule of thumb: run the tool on whichever side has network reach to the other.

You want to … Run the tool on Initiates outbound to Works without a tunnel?
Push local → prod mcp__memex-local__mirror (direction=push) https://memex.meshweaver.cloud ✅ public HTTPS
Pull prod → local mcp__memex-local__mirror (direction=pull) https://memex.meshweaver.cloud ✅ public HTTPS (local pulls in)
Push prod → local (run on prod) mirror direction=push localhost ❌ prod can't reach localhost
Pull local → prod (run on prod) mirror direction=pull localhost ❌ same

For the third and fourth cases, expose your local instance with a Cloudflare tunnel or ngrok and use the public URL as the remote.

Step-by-step recipe — push local content to prod

1. Issue an ApiToken on the destination portal

Open the destination portal (e.g. https://memex.meshweaver.cloud), log in as the user the import should run as, and:

2. Dry-run from the source

Always preview before writing. Pass dryRun=true to enumerate the subtree without touching the destination:

mcp__memex-local__mirror
    direction="push"
    remote="prod"                       # a configured Mirror:Remotes profile — keeps the token server-side
    sourcePath="rbuergi/Story"
    targetPath="rbuergi/Story"
    dryRun=true

Example response:

{
  "status": "DryRun",
  "direction": "Push",
  "sourcePath": "rbuergi/Story",
  "targetPath": "rbuergi/Story",
  "nodesScanned": 4,
  "paths": [
    "rbuergi/Story/KernelTour",
    "rbuergi/Story/KernelTour/01-Code",
    "rbuergi/Story/KernelTour/02-Activity",
    "rbuergi/Story/KernelTour/03-NodeTypes"
  ]
}

Read the list. Confirm the count and paths match your expectations before proceeding.

3. Execute for real

Same call, dryRun=false (the default):

mcp__memex-local__mirror
    direction="push"
    remote="prod"
    sourcePath="rbuergi/Story"

Example response:

{
  "status": "Ok",
  "direction": "Push",
  "sourcePath": "rbuergi/Story",
  "targetPath": "rbuergi/Story",
  "nodesImported": 4,
  "nodesSkipped": 0,
  "nodesRemoved": 0,
  "partitionsImported": 0,
  "elapsedMs": 412
}

4. Verify on the destination

mcp__memex-prod__search query="namespace:rbuergi/Story scope:subtree"

This should return the four nodes. You can also open https://memex.meshweaver.cloud/rbuergi/Story/KernelTour directly in a browser.

Pulling from a remote into local

Pull is the same tool with direction="pull" — there is no separate tool. Here local makes outbound calls to prod, fetches the subtree, and writes it under the target path:

mcp__memex-local__mirror
    direction="pull"
    remote="prod"
    sourcePath="Doc/Architecture/GrantingAccess"
    targetPath="rbuergi/MyDocs/GrantingAccess"
    dryRun=true

Flags

Flag Default Effect
dryRun false Enumerate without writing. Safe to run any time.
removeMissing false Destructive. Delete destination nodes that don't exist on the source. Use only when you want the destination to mirror the source exactly.
targetPath sourcePath Write under a different path on the destination — useful for sandbox copies (e.g. rbuergi/Storyrbuergi/Story-staging).

Authentication and access scope

The destination's ApiTokenAuthenticationHandler validates the token and stamps the user's ObjectId onto every per-node write. The mirror runs as the user who issued the token. That user must have:

Note (intended): a destination user lacking Create on a path should surface as nodesSkipped, not a hard failure. Until the handler lands this is a contract statement, not observed behaviour. Run with the destination's Admin role during development to avoid surprises.

What does NOT cross instances (v1)

Not everything survives a mirror. Content that lives outside node.Content is out of scope for v1:

Token economy

Each mirror invocation from Claude Code is one MCP tool call: approximately 1 k input tokens (args) plus a short text summary back. The actual recursive copy runs server-side — Claude isn't reasoning node by node.

If you want strictly zero LLM tokens, the same MirrorRequest is also posted by the import dialog UI (ImportLayoutArea, Blazor) and by the REST endpoint. Future work includes a CLI that drives it without an LLM in the loop.

Component Path
Request/response contract (MirrorRequest/MirrorResult) src/MeshWeaver.Mesh.Contract/Services/IMirrorOperations.cs
HTTP storage adapter src/MeshWeaver.Hosting/Persistence/Http/HttpMeshStorageAdapter.cs
MCP transport src/MeshWeaver.Hosting/Persistence/Http/McpRemoteMeshClient.cs
Path remapping (source → target prefix) src/MeshWeaver.Hosting/Persistence/Http/PathRemappingStorageAdapter.cs
MCP tool (Mirror, one tool with direction=push\|pull) src/MeshWeaver.Mcp/McpMeshPlugin.cs
REST endpoint (POST /api/mesh/mirror) memex/Memex.Portal.Shared/Api/MeshApiEndpoints.cs
Mesh-hub handler not yet registered — see status note
Tests test/MeshWeaver.Hosting.Test/HttpMeshStorageAdapterTests.cs · MirrorOperationsTests.cs (class MirrorRequestValidationTests — request/result contract only; no end-to-end flow, since there is no handler)
Auth handler memex/Memex.Portal.Shared/Authentication/ApiTokenAuthenticationHandler.cs

Troubleshooting

Symptom Likely cause
401 Unauthorized on every call ApiToken expired or revoked, or the owning user has been disabled. Re-issue.
nodesSkipped is non-zero Destination user lacks Create/Update on the failing paths. Check AccessAssignments on the destination (GrantingAccess.md).
Empty nodesImported for a non-empty path sourcePath doesn't match anything. Verify with mcp.search namespace:{sourcePath} scope:subtree on the source side first.
Partition data missing on destination Expected in v1 — only inline node.Content is mirrored. Use the local ZIP export/import for satellite-table data.
Hung or 30 s+ for a small subtree Remote is in a bad state (recently restarted; cold-grain activation). Retry once.
Reconnecting…
The server was updated. Reloading the page to pick up the latest version.