{"id":50318682,"url":"https://github.com/dpup/meshcore-mcp","last_synced_at":"2026-05-31T22:00:54.181Z","repository":{"id":361043251,"uuid":"1252858020","full_name":"dpup/meshcore-mcp","owner":"dpup","description":"Model Context Protocol (MCP) server exposing a MeshCore node — and the mesh reachable through it — to AI agents and tools.","archived":false,"fork":false,"pushed_at":"2026-05-29T00:26:08.000Z","size":303,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-29T02:08:22.559Z","etag":null,"topics":["agent","ai","lora","mcp","mesh-networking","meshcore","model-context-protocol","nodejs","off-grid","radio","typescript"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dpup.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-05-29T00:11:40.000Z","updated_at":"2026-05-29T00:26:12.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dpup/meshcore-mcp","commit_stats":null,"previous_names":["dpup/meshcore-mcp"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/dpup/meshcore-mcp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dpup%2Fmeshcore-mcp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dpup%2Fmeshcore-mcp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dpup%2Fmeshcore-mcp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dpup%2Fmeshcore-mcp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dpup","download_url":"https://codeload.github.com/dpup/meshcore-mcp/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dpup%2Fmeshcore-mcp/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33750474,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-31T02:00:06.040Z","response_time":95,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent","ai","lora","mcp","mesh-networking","meshcore","model-context-protocol","nodejs","off-grid","radio","typescript"],"created_at":"2026-05-29T02:01:47.488Z","updated_at":"2026-05-31T22:00:54.174Z","avatar_url":"https://github.com/dpup.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# meshcore-mcp\n\n\u003e A Model Context Protocol server that exposes a [MeshCore](https://meshcore.co.uk)\n\u003e node — and the mesh reachable through it — as a clean, high-signal interface\n\u003e for any AI agent or tool.\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)\n[![Types: included](https://img.shields.io/badge/types-included-blue.svg)](#)\n[![Node](https://img.shields.io/badge/node-%E2%89%A518-brightgreen.svg)](#)\n[![Module: ESM](https://img.shields.io/badge/module-ESM-f7df1e.svg)](#)\n\n**Quick start** — add it to Claude Code pointed at a node, then ask *\"survey the mesh\"*:\n\n```sh\nclaude mcp add meshcore --env MESHCORE_HOST=\u003cnode-ip\u003e -- npx -y @dpup/meshcore-mcp\nclaude -p 'survey the mesh'\n```\n\n`meshcore-mcp` wraps a [`@dpup/meshcore-ts`](https://github.com/dpup/meshcore-ts)\n`MeshCoreClient` behind a small, deliberately shaped surface of MCP **tools,\nresources, and prompts**. Point an MCP client at a node and operate the mesh in\nnatural language — survey nodes, read recent traffic, send messages, and run\ncurated admin commands — with structured, digested results instead of raw frames.\n\nIt is the **device layer**, and **ungated by design**: no conversation policy,\nno admin-channel gate, no coalescing, no autonomous behavior. A human at Claude\nCode *is* the policy, and approves each step; an autonomous agent brings its own.\nSame server, correct in both cases — its job is\nto be a faithful, well-shaped device interface, and to mark read-versus-action on\nevery tool so a consuming policy layer, or a reviewing human, can reason about\nsafety mechanically.\n\n\u003e [!WARNING]\n\u003e **The commands are ungated.** `send_message`, `set_channel`, `delete_channel`,\n\u003e `trace_path`, and `admin` transmit on the mesh and change device state with **no\n\u003e built-in approval or policy layer** of their own. With **Claude Code** you\n\u003e approve every tool call — that human-in-the-loop *is* the gate, and what makes\n\u003e interactive use safe. For **autonomous or headless** use, put your own policy\n\u003e layer in front (that is `meshcore-elmer`'s job) — don't point an unattended\n\u003e agent at a live mesh without one. Every tool is annotated read-only vs. action\n\u003e (`readOnlyHint` / `destructiveHint`) so a policy layer, or a reviewing human,\n\u003e can reason about safety mechanically.\n\n## Use it with Claude Code\n\n`meshcore-mcp` is a local-process server (stdio). The simplest consumer is a\nhuman operator with Claude Code: add one entry to your MCP configuration,\npointing it at a node, and operate the mesh in plain language — Claude Code\nprompts before every tool call, so no policy layer is needed (see the warning\nabove).\n\n**Pick the config that matches how your node is flashed.** A MeshCore node runs\n*either* a WiFi companion firmware (`companion_radio_wifi`, reached over TCP/IP)\n*or* a USB/serial companion firmware — one transport per build. Set the matching\nvariable; setting both is rejected at startup with a legible error.\n\n**WiFi / IP companion** — point it at the node's address (default port `5000`):\n\n```json\n{\n  \"mcpServers\": {\n    \"meshcore\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@dpup/meshcore-mcp\"],\n      \"env\": {\n        \"MESHCORE_HOST\": \"192.168.1.50\",\n        \"MESHCORE_PORT\": \"5000\"\n      }\n    }\n  }\n}\n```\n\n**USB / serial companion** — point it at the device path (`/dev/ttyACM0` on\nLinux, `/dev/tty.usbmodemXXXX` on macOS, `COM3` on Windows):\n\n```json\n{\n  \"mcpServers\": {\n    \"meshcore\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@dpup/meshcore-mcp\"],\n      \"env\": {\n        \"MESHCORE_SERIAL_PATH\": \"/dev/ttyACM0\"\n      }\n    }\n  }\n}\n```\n\n\u003e Serial needs Node's native `serialport` bindings — the published `npx` / binary\n\u003e path runs on Node, so this just works (the `bun` dev entrypoint can't load the\n\u003e serial native module).\n\nThe server reads its home node and credentials from the environment its launcher\nhands it; a malformed or missing config exits non-zero with a legible message\n(never a stack trace).\n\n| Variable | Meaning |\n| --- | --- |\n| `MESHCORE_HOST` | Home node TCP host — the `companion_radio_wifi` path. Set this **or** `MESHCORE_SERIAL_PATH`, not both. |\n| `MESHCORE_PORT` | TCP port for `MESHCORE_HOST` (default `5000`). |\n| `MESHCORE_SERIAL_PATH` | USB serial device path (e.g. `/dev/ttyACM0`) — the serial alternative to `MESHCORE_HOST`. |\n| `MESHCORE_LOGIN_PASSWORD` | Default login/admin password for remote nodes (default `\"\"` — the guest password). |\n| `MESHCORE_LOGIN_PASSWORD_FILE` | Read the default password from a file instead (its trailing newline is stripped). Set this **or** `MESHCORE_LOGIN_PASSWORD`, not both. |\n| `MESHCORE_NODE_PASSWORDS` | JSON object of per-node overrides: `{ \"rocky-ridge\": \"secret\" }` (keyed by node id or name). |\n| `MESHCORE_NODE_PASSWORDS_FILE` | Read that same JSON map from a file instead — keeps node secrets out of the environment and your MCP config (`chmod 600` it). Set this **or** `MESHCORE_NODE_PASSWORDS`, not both. |\n| `MESHCORE_REQUEST_TIMEOUT_MS` | Device request timeout, ms (default `10000`). |\n| `MESHCORE_TRAFFIC_CAPACITY` | Recent-traffic ring-buffer size (default the buffer's own default). |\n| `MESHCORE_ADMIN_REPLY_TIMEOUT_MS` | How long the remote-admin path waits for a CLI reply, ms (default `15000`). |\n\nThe `--host`, `--port`, and `--serial` flags override the corresponding env vars.\n\n### Try it with no radio (simulator)\n\nNo MeshCore hardware? [`examples/sim-server.ts`](./examples/sim-server.ts) serves\nthe exact same MCP surface over stdio, but backed by\n[`@dpup/meshcore-sim`](https://github.com/dpup/meshcore-sim) over a small\nsimulated mesh — with a real-time clock so live traffic actually flows while you\npoke at it. Point Claude Code at it:\n\n```sh\ngit clone https://github.com/dpup/meshcore-mcp \u0026\u0026 cd meshcore-mcp \u0026\u0026 bun install\nclaude mcp add meshcore-sim -- bun \"$(pwd)/examples/sim-server.ts\"\n```\n\nThen ask Claude to *\"survey the mesh\"*, *\"check the health of Rocky Ridge\"*,\n*\"show recent traffic\"*, or *\"preview an admin reboot of Rocky Ridge\"*. The\nproduction binary (`src/cli.ts`) talks only to real devices; the simulator is a\ndev dependency and never ships — this entrypoint is the hardware-free way to try\nthe server. Remote `admin` execution round-trips too: the sim-server configures\nreactive responders (meshcore-sim ≥ 0.2.0), so `login → CliData → reply` returns\na plausible CLI reply instead of timing out.\n\n## The surface\n\nA short, action-oriented surface: a handful of well-shaped tools beats dozens of\nfine-grained ones. Reads are exposed as **read-only tools** (not only as\nresources) because tools are what every MCP client reliably surfaces to the model\nfor active querying. The `login` / `logout` handshake is never exposed — it is\nmechanical, and lives inside the tools that need it.\n\n### Tools\n\n| Tool | Intent | Annotations |\n| --- | --- | --- |\n| `get_node_health(node?)` | Consolidated health snapshot — identity, radio, battery, uptime/queue, packet/radio stats. Omit `node` for the home node; pass a contact name or hex key prefix for a remote. Hides the home-vs-remote distinction and any remote login. | read-only · idempotent |\n| `survey_mesh()` | One roster of the home node and every known contact, with advertised role and last-heard time — for spotting quiet or missing nodes. | read-only · idempotent |\n| `get_recent_traffic(since?)` | Recent live mesh traffic from the rolling buffer, oldest→newest, each tagged with structural provenance. `since` (ISO-8601 or epoch-ms) windows it. | read-only · idempotent |\n| `send_message(target, text)` | Transmit a message to a contact (name / hex prefix) or channel (`#name`, `#index`, or a bare index). | action · **not idempotent** — a resend is a second transmission |\n| `admin(node, command, params?, dryRun?)` | Run one enumerated admin command against a node, home or remote; collapses the remote `login → CliData → reply` handshake. `dryRun: true` previews the intent without contacting the device. | **destructive** (statically conservative); per-command risk tier in the result |\n\n`admin`'s `command` is drawn from an **enumerated, curated set** of 16 commands —\nsee the [guide](./docs/guide.md#the-admin-command-set) — extensible, but never\nfree-form text. Each command declares a **risk tier** (`benign` · `config` ·\n`sensitive` · `destructive`) that maps deterministically to per-command\nannotations, surfaced in the structured result.\n\n### Resources\n\n| Resource | URI | Notes |\n| --- | --- | --- |\n| Live mesh traffic | `meshcore://traffic/live` | **Subscribable.** A rolling feed of inbound events; the server pushes `notifications/resources/updated` to subscribers, who then re-read to fetch. Every event carries provenance — sender, channel identity, and **decrypt-verification**. |\n| Mesh roster | `meshcore://nodes` | Pull-style: the home device plus every known contact, each with role and last-heard time. |\n| Contacts | `meshcore://contacts` | Pull-style: the home node's stored contact list — name, public key, role, location, last-heard. |\n\nDecrypt-verification is **structural**, not a wire flag: `meshcore-ts`\ndistinguishes a verified `channelMessage` from an unverified `channelData`\ndatagram (and a direct `contactMessage` from either), and the stream preserves\nthat distinction. That is exactly what lets a downstream gate's negative cases be\nrepresentable — an unverified admin-channel datagram never surfaces as a verified\nchannel message.\n\n### Prompts\n\n| Prompt | Args | Frames |\n| --- | --- | --- |\n| `morning-mesh-check` | — | A daily health sweep: survey the roster, flag quiet nodes, spot-check the suspicious ones. |\n| `diagnose-quiet-node` | `node` | Work out why a node has gone quiet — health, recent traffic, last-heard, context. |\n| `draft-outage-notice` | `node`, `window?` | Draft a concise outage notice over a time window, and send it on approval. |\n\nA prompt **frames; it does not freeze**: each poses a well-formed task and points\nthe agent at the real tools by name, and the agent still reasons freely. No\npolicy, no secrets.\n\n## Install\n\n`meshcore-mcp` is an **MCP server**, not a library you import — \"installing\" it\nmeans registering its launch command with an MCP client.\n\n- **Claude Code** — `claude mcp add` (see [Use it with Claude Code](#use-it-with-claude-code)\n  above). The client launches the server on demand via `npx`; nothing to install\n  globally.\n- **Any other MCP client** (Cursor, Windsurf, …) — put the same command in that\n  client's MCP config: `npx -y @dpup/meshcore-mcp`, with the `MESHCORE_*` env vars\n  above.\n- **Prefer a pinned, on-PATH binary?** Install it globally and point the client's\n  `command` at `meshcore-mcp`:\n\n  ```sh\n  npm i -g @dpup/meshcore-mcp      # or: bun add -g / pnpm add -g @dpup/meshcore-mcp\n  ```\n\nESM-only, **Node.js ≥ 18**.\n\n\u003e **Embedding** the server in your own host (the `createServer` API)? Add it as a\n\u003e dependency instead — `npm install @dpup/meshcore-mcp` — and see\n\u003e [Quickstart — embed a sim-backed server](#quickstart--embed-a-sim-backed-server).\n\n## Documentation\n\n- **[Guide](./docs/guide.md)** — concepts and recipes: configuring the home node,\n  the tool surface and annotations, the live stream and provenance, the `admin`\n  set and dry-run, the three consumers, and testing your own agent against a\n  sim-backed server.\n- **[API reference](./docs/api.md)** — the complete, generated reference: every\n  exported symbol, with full signatures and types.\n\n## Quickstart — embed a sim-backed server\n\nYou can drive the whole server in-process with **no hardware**, against\n[`@dpup/meshcore-sim`](https://github.com/dpup/meshcore-sim): inject a\n`SimConnection` where production would build `MeshCoreClient.tcp(host, port)`,\ndrive a virtual `SimClock`, and call tools through a real in-memory MCP `Client`.\nNothing below `MeshService` can tell a sim from a radio — this is the seam the\nwhole test strategy hangs on.\n\nAdd it as a dependency first — `npm install @dpup/meshcore-mcp` — then:\n\n```ts\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { InMemoryTransport } from \"@modelcontextprotocol/sdk/inMemory.js\";\nimport { MeshCoreClient } from \"@dpup/meshcore-ts\";\nimport { SimClock, SimConnection, defineWorld, node, contact } from \"@dpup/meshcore-sim\";\nimport { createServer, MeshService } from \"@dpup/meshcore-mcp\";\n\n// 1. A simulated mesh — no radio. The home node plus one contact.\nconst world = defineWorld({\n  homeNodeId: \"home\",\n  nodes: [node(\"home\", { name: \"Base\" }), node(\"alice\", { name: \"Alice\" })],\n  contacts: [contact(\"Alice\", \"alice\")],\n});\n\n// 2. The injected clock the whole server takes its time from.\nconst clock = new SimClock();\n\n// 3. A real MeshCoreClient over the sim Connection — the production seam.\nconst meshClient = new MeshCoreClient(new SimConnection({ world, clock }).asConnection(), {\n  autoSync: true,\n});\nconst service = new MeshService(meshClient, clock);\nawait service.start();\n\n// 4. Wire createServer to an in-memory MCP Client (no sockets, no stdio).\nconst server = createServer({ service });\nconst client = new Client({ name: \"demo\", version: \"0.0.0\" });\nconst [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();\nawait Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);\n\n// 5. Call a tool exactly as Claude Code would.\nconst health = await client.callTool({ name: \"get_node_health\", arguments: {} });\nconsole.log(health.structuredContent);\n\nawait client.close();\nawait server.close();\nawait service.stop();\n```\n\nIn production, `cli.ts` does the same wiring over `MeshCoreClient.tcp(host, port)`\nand a `SystemClock`, served on a `StdioServerTransport` — that's what the\n`meshcore-mcp` binary (and the Claude Code config above) launches.\n\n## See it in action\n\n[`examples/demo.ts`](examples/demo.ts) is a guided tour — it builds a world,\ndrives the real server through an in-memory MCP `Client`, and walks the whole\nfeature set: tools (home + an offline repeater failing cleanly), the live stream\nwith verified vs. unverified provenance side by side, and an `admin` dry-run\npreview then a scripted exec. No hardware, fully deterministic:\n\n```sh\nbun examples/demo.ts          # or --seed \u003cn\u003e / a positional seed\n```\n\n\u003e In a real terminal the tour is ANSI-colored; re-run it with any `--seed` and\n\u003e the output is identical every time.\n\n## Develop\n\n```sh\nbun install\nbun run typecheck   # tsc --noEmit (strict)\nbun run test        # vitest — incl. full-stack tests over a sim-backed server\nbun run build       # emit dist/ (ESM + .d.ts + the meshcore-mcp binary)\nbun run dev         # run the server over stdio\nbun run docs        # regenerate docs/api.md from the source\n```\n\nSee [AGENTS.md](./AGENTS.md) for architecture and contribution notes.\n\n## Design notes\n\n- **Two contracts, one server.** `meshcore-mcp` is a thin server wedged between\n  the **MCP protocol** above (`@modelcontextprotocol/sdk`'s `McpServer`) and the\n  **`@dpup/meshcore-ts` device client** below. It owns neither. Almost every\n  design decision falls out of taking both seriously.\n- **Injected client + clock.** `MeshService` takes its `MeshCoreClient` and\n  `Clock` by **injection** — production builds `MeshCoreClient.tcp(host, port)`\n  with a `SystemClock`; tests build `new MeshCoreClient(sim.asConnection())` with\n  a `SimClock`. Nothing below `MeshService` knows which it got, and time always\n  comes from the clock — never raw `Date.now()` / `setTimeout`.\n- **Provenance is a hard requirement.** The live stream carries each event's\n  sender, channel identity, and structural decrypt-verification, because a\n  downstream policy layer can only function if it knows which channel a message\n  genuinely arrived on.\n- **Ungated by design.** No policy, no admin-channel gate, no scheduling, no\n  autonomous behavior. That belongs to a consumer; keeping the server free of it\n  is exactly what lets the same artifact serve a human at a terminal and an\n  autonomous agent without compromise.\n\n## License\n\n[MIT](./LICENSE) © Dan Pupius\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdpup%2Fmeshcore-mcp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdpup%2Fmeshcore-mcp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdpup%2Fmeshcore-mcp/lists"}