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 insrc/ormemex/registers one, and theAddMirrorHandlerthat several code comments point at does not exist. The contract (MirrorRequest/MirrorResultinsrc/MeshWeaver.Mesh.Contract/Services/IMirrorOperations.cs), themirrorMCP tool, thePOST /api/mesh/mirrorendpoint, the import-dialog caller, and the HTTP transport are all in place, but a postedMirrorRequestgets 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
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):
- The MCP tool resolves the remote profile and posts one
MirrorRequestat themeshhub (hub.Observe<MirrorResult>). - The mesh-hub handler reads every node under
sourcePathand, per node, calls the destination's MCP surface (create/update) throughMcpRemoteMeshClient, authenticating with the destination's ApiToken. - It returns a
MirrorResultwith the fieldsstatus,direction,sourcePath,targetPath,nodesImported,nodesSkipped,nodesRemoved,partitionsImported, andelapsedMs, 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:
- Navigate to Settings → API Tokens (or
/me/Settings/ApiTokens). - Click Create token, name it (e.g.
mirror-from-local-2026-05), scope it to the user, and copy themw_…value. - Tokens are revocable from the same page; rotate after one-shot mirrors.
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/Story → rbuergi/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:
Permission.Readon the source paths (on the source portal — usually trivially satisfied when operating on your own partition).Permission.Create/Updateon the destination paths.
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:
- Partition objects — Activity messages, Comment trees, and satellite-table data are not included. The MCP tool surface doesn't yet expose a generic enumerate-partition-objects call. Inline
node.Contentpayloads (Markdown, Code, NodeTypeDefinition, etc.) survive cleanly; satellite data does not. - Compiled assemblies — destination NodeTypes recompile from source on first access. Trigger early with
mcp__memex-prod__compile @rbuergi/MyTypeto warm the cache before any UI hits the type. - API tokens themselves — tokens are partition-scoped and cannot be mirrored across portals. Issue a fresh token on the destination.
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.
Source links
| 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. |
Related
- GrantingAccess.md — how to issue and audit AccessAssignments.
- PostgresSchemaArchitecture.md — partition-scope model.