# Native MeshForge MCP — bounded public read subset Status: implemented experimental HTTP adapter, pinned to MCP **2025-03-26**. Go standard library only; no MCP SDK, npm package, browser dependency or external module. This is not a full MCP conformance or private authorization claim. No wallet, repository execution, filesystem access, private content or write tool is exposed. The existing public discovery cache is the only data source. The reviewed primary references are the official [2025-03-26 base protocol](https://modelcontextprotocol.io/specification/2025-03-26/basic), [lifecycle](https://modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle), [HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports), [tool protocol](https://modelcontextprotocol.io/specification/2025-03-26/server/tools) and [typed schema](https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/2025-03-26/schema.ts). Newer protocol revisions exist; this adapter does not silently claim their semantics. ## Requests and compatibility `POST /mcp` accepts UTF-8 `application/json` (optional UTF-8 charset). Replies are JSON. GET/DELETE/OPTIONS/other methods return 405 with `Allow: POST`; there is no SSE stream, transport session, resumability or server-initiated interaction. Origin checks also run before rejecting non-POST methods when routed through `mcpMethod`. No query parameters or noncanonical raw URL path aliases are accepted. After initialization, a supplied `MCP-Protocol-Version` must be exactly `2025-03-26`. The initial initialize request may carry its bounded offered version in that header as well as its body; its result still selects `2025-03-26`. Missing version headers remain compatible with existing clients of this implementation. Clients should send `Accept: application/json, text/event-stream`; the server always chooses JSON. For legacy machine clients, an absent Accept header or a JSON-compatible wildcard is tolerated. An explicit exclusion of JSON returns 406. The server does not claim full Streamable HTTP compatibility on that basis. The supported methods are: | Method | Input / result | |---|---| | `initialize` | Required `protocolVersion`, object `capabilities`, and `clientInfo` with nonempty `name`/`version`; returns MeshForge version and only the tools capability | | `notifications/initialized` | No ID, optional empty params/allowed metadata; accepted with HTTP 202 and an empty body | | `ping` | No params or empty/allowed metadata object; empty result object | | `tools/list` | Two fixed read-only tools and bounded argument schemas; no further catalog page | | `tools/call` | Known public tool name and strictly checked arguments; text content containing a JSON array | Initialization requires a bounded, nonempty version offer and valid capability shape. Even an unknown offer receives the server's supported `2025-03-26` in the result; the client must support that selected version or disconnect. This follows the pinned lifecycle's version-negotiation rule and does not activate the offered version. The adapter remains stateless: it does not enforce a cross-request initialize/initialized session lifecycle or session-wide ID reuse tracking. Tool calls without a prior HTTP initialize remain compatible public reads. Unknown requests produce a JSON-RPC method/parameter error. Unsupported notifications, including cancellation for nonexistent tracked sessions, receive an HTTP error without a JSON-RPC response. They are never automatically acknowledged merely because an ID is absent. Request IDs are strings of at most 128 UTF-8 bytes or canonical decimal integers within the exact interoperable range `[-9007199254740991, 9007199254740991]`. Null, booleans, objects, arrays and noncanonical numeric forms (including fractional/exponent spellings) are rejected. Valid IDs are reflected in replies. Invalid requests use the usual null error correlation ID, not a successful response to a null request ID. Unknown field names and case aliases are rejected, including nested method parameters. The complete JSON document is checked for duplicate decoded object keys (also escaped-key aliases), trailing documents, UTF-8 validity, token sizes and nesting depth before dispatch. `_meta.progressToken` accepts a bounded string/integer but does not enable progress streams. Client experimental capabilities are bounded declarations, not granted permissions. Nothing in client capabilities can add tools or access a private repository. ## Bounded batches The pinned protocol's batch form is supported, with 1–16 messages per POST. Initialization must remain a separate request. Duplicate request IDs in a batch are rejected before tool work; equivalent escaped string IDs count as duplicates. Nested batches are not supported as messages. Valid notifications do not produce JSON-RPC responses. A batch containing only accepted initialized notifications returns 202 with no body. A batch with an unsupported notification is rejected at the HTTP layer before dispatch, rather than fabricating notification results. Each message consumes a rate unit, including malformed messages in an otherwise parseable batch. A batch receives one fixed total response budget, divided before work; it cannot multiply the byte limit by sixteen. Large individual records may not fit a large batch's per-response allowance. That call returns a bounded error asking the client to reduce batch size; it never truncates a record or silently skips it. An ordinary single call has the full page allowance. One public event snapshot is loaded lazily per HTTP batch, and the repository index is derived at most once. Multiple tool calls do not repeatedly reload or marshal the whole cache. The snapshot remains consistent within that HTTP batch. ## Public tools and pagination `discover_repositories` accepts optional `category`, `cursor`, and `limit`. `get_discovery_events` accepts optional `cursor` and `limit`. Categories compare exactly against public repository metadata. Unsupported arguments fail; there is no path, repo-working-directory, private registry or command argument. Default limit: 50; maximum: 100 items. Text content remains a JSON array, including exactly `[]` for an empty valid public cache, preserving the prior result shape. Repository pages use ascending repository ID; event pages use ascending event ID. This explicitly differs from the UI's publisher-time feed ordering. Tool results contain extension metadata in the standard `_meta` container: ```json { "content": [{"type": "text", "text": "[]"}], "_meta": { "meshforge/pageCount": 0, "meshforge/cache": "incomplete-local-public-cache" } } ``` When another page exists, `_meta["meshforge/nextCursor"]` contains the next opaque cursor. Pass it back as `arguments.cursor` to the same tool and category. Its bounded canonical encoding binds the tool, filter and last returned ID; wrong tool/filter, malformed hashes, noncanonical base64 or unknown fields fail. The cursor is a public read selector, **not an authentication token**. Its bytes need no signature because they grant no access beyond that already-public query. Only selected page entries and at most one size lookahead are serialized. The server does not serialize the entire dataset to decide page size and serialize it again for the reply. A byte-full page resumes after its last included item; no omitted item is lost between pages of an unchanged cache. Across separate HTTP requests the cache can change, so this is not a durable database snapshot: new IDs preceding a consumed cursor require a later rescan. Cached metadata is not a complete view of a global network. A missing/corrupt public cache produces an error, not a misleading successful empty list. Descriptions and signed metadata remain untrusted **data**, not instructions for an agent. The tool schema and text-only results do not execute, embed or interpret repository HTML/scripts. Signature validity does not confer real-world identity or prompt authority. Clients still need their own treatment of untrusted content. ## HTTP security and resource ceilings Browser origins are allowed only through the existing direct-loopback peer, loopback Host and exact matching Origin boundary. An Origin matching an arbitrary attacker-controlled Host is insufficient. Duplicate/null/foreign origins, forwarding headers and cross-site Fetch Metadata are rejected. No public browser Origin allowlist is configured; public browser origins therefore fail closed. Public machine-to-machine reads without Origin remain possible. A loopback connection with a non-loopback Host is rejected even without Origin. There is no CORS relaxation and no reverse-proxy-origin inference. This is a public reader, not authentication for named people/devices. It exposes no private ACL or content and never reads `private-repos.json`. Local private HTML controls and the separate authenticated public object transport are not reused as implicit MCP permissions. Production remote identity/AuthZ, OAuth/session integration and full lifecycle/interoperability assessment remain separate gates. | Resource | Ceiling | |---|---| | Request body | 64 KiB total, including a batch | | JSON nesting / object members / array entries | depth 16 / 64 per object / 256 per array; outer batch at most 16 | | JSON strings / numeric tokens | 8192 bytes per token / 128 bytes | | In-flight POSTs | 4 per Hub, rejected before reading an additional body | | Request budget | 256 message units per source IP/minute; 2048 globally/minute | | Rate-accounting IPs | 4096 maximum; expired windows pruned | | Page payload | at most 96 KiB of JSON-array text and 100 items | | Entire HTTP JSON reply | at most 256 KiB, including all batch responses | | Underlying public cache | existing 16 MiB/100000-event store limits | The admission lock is separate from the cache mutex: cache verification cannot make unlimited new requests wait before concurrency admission. Busy/rate-limited requests return 503/429; rate responses include `Retry-After: 60`. Invalid POSTs consume rate units too. Page iteration checks request cancellation. The enclosing HTTP server's existing timeouts still apply. These are explicit bounds, not a production throughput or distributed anti-Sybil guarantee. ## Validation and remaining limits Fixture tests cover initialization and legacy empty-array tool results; strict IDs/version/parameters, duplicate keys and trailing JSON; origin/DNS-rebinding and forwarding rejection; 1005 large signed events paginated completely for both tools and category filtering; response ceilings and selected-entry serialization; cursor binding; shared batch snapshots; batch notification/ID/initialization rules; per-message rate accounting; four genuinely blocked body readers and a fifth denial before reading; malformed-request rate pressure; cancellation; private registry absence and corrupted-cache failure. No test reads real private repositories. Root-owned independent review cases guard Origin and malformed-RPC regressions. A bounded decoder fuzz target is included. No SDK/conformance certification, private tools, stdio transport, SSE, complete stateful lifecycle, tasks, sampling, resources, prompts, approvals or execution is implemented by this adapter. Unknown optional capabilities are not simulated. External security review and real integration testing remain necessary before a public production claim.