Skip to content

feat(mcp): discovery surfaces, registry publish, and the stdio gateway (#9526) - #9735

Merged
JSONbored merged 6 commits into
mainfrom
feat/mcp-discovery-gateway-9526
Jul 29, 2026
Merged

feat(mcp): discovery surfaces, registry publish, and the stdio gateway (#9526)#9735
JSONbored merged 6 commits into
mainfrom
feat/mcp-discovery-gateway-9526

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Closes #9526.

What changed

The last mile of #9515: LoopOver's MCP servers now advertise themselves, publish to the registry, and sit behind one client entry.

Discovery surfaces, computed at request time. /.well-known/mcp.json plus the /.well-known/agent-tools/{index,openai,anthropic}.json trio, built from the #9518 contract registry on every request — never a committed artifact. That is metagraphed's lesson taken literally: its committed server card made every concurrent tool PR conflict on the same generated file. There is nothing here to regenerate and nothing a tool PR can conflict on. Each document carries a weak ETag and answers 304 on a matching if-none-match, and generated_at is derived from the deploy's version rather than the clock, because a wall-clock field would change the ETag on every request and leave the 304 path dead code.

The routes report the deployment that answered. src/server.ts serves this same Hono app, so the first cut's hardcoded "cloud" meant a self-host card advertised the cloud's tool set — a list of calls that 404 there.

server.json + an OIDC publish workflow, validated field-by-field against the tri-lock, with the anti-rot guard the issue asked for: the check fails loudly if any path it watches is missing from HEAD. metagraphed's version-sync workflow rotted silently for months watching a path that had been renamed away, and a workflow that can quietly watch nothing is worse than no workflow. serverInfo.version no longer says "0.1.0" forever; it tracks @loopover/mcp's released version.

Gateway mode for the stdio bin. With a loopover-mcp login session, the stdio server discovers the remote server's tools/list and mounts every tool it does not already serve. One config, every tool your session entitles you to — rather than a local entry for the local-git tools and a separate remote endpoint for the rest.

The mount is best-effort by construction. No session, no network, or a hostile response all leave a server that starts and lists its local tools plus one advisory resource explaining how to get the rest; --no-remote opts out to byte-identical pre-gateway behavior. An enhancement that can break startup is a liability, and a contributor on a plane still needs their local tools. Name collisions cannot happen — one registry, one entry per name, so a name is either local-git or remote — and validate:mcp now asserts that invariant; the mount still skips an already-registered name so a future violation degrades to "local wins" instead of crashing on a duplicate registration.

Client config, derived. The three connection modes across five hosts were written out by hand in four places — clientSnippet() in the bin, mcp-clients.mdx, and both package READMEs — and had already drifted; the docs page documented no remote auth at all. The grid is now data in @loopover/contract, init-client --print renders it, and a generator writes the docs and README blocks from it under a --check drift guard. init-client gains --mode stdio|remote|miner, defaulting to stdio so a pre-gateway invocation prints exactly what it always did. A remote entry names the token's environment variable in whichever dialect the host reads and never carries a value; a host whose remote dialect this repo cannot vouch for is refused rather than handed a plausible snippet that fails on paste.

Transport-tagged telemetry. surface says which server was asked; only the new transport says whether a stdio call ran against the local checkout or was forwarded — so gateway adoption is exactly the count of stdio + proxied, and was unmeasurable without it. Proxied tools now run through the same telemetry chokepoint local ones do.

AMS bin decision, recorded. loopover-miner-mcp stays a separate stdio server, per #9184's constraint that local UX must not regress. The dual-role two-entry config is generated from the same grid and pin-tested.

Fix-what-you-find

Three instances of the same rot, all found by this work:

  • The vitest alias list for @loopover/contract was typed out one subpath at a time; adding an export without its alias produced a Cannot find package naming the symptom and not the cause. Now derived from the package's own exports map.
  • The published-tarball allowlist had failed the same way every previous time — by under-listing, so a legitimately shipped lib/*.js tripped the gate that exists to catch strays. Now derived from the committed sources, which keeps the property that matters: the tarball may contain the compiled form of committed source and nothing else.
  • registerProxiedTool passed contract.input.shape where the rest of the bin passes the schema object. The SDK re-wraps a raw shape in a plain z.object and drops the catchall, turning every extra field the remote allows into a -32602 the caller cannot act on.

Plus: the mount moved after connect() and is no longer awaited, so a network round-trip no longer sits between a client launching the process and it answering anything — and registering a tool on a connected server emits notifications/tools/list_changed, which is the refresh path a stateless remote cannot push on its own.

Validation

  • npx vitest run --changed=origin/main: 24708 passed, 21 skipped, 1295 files — clean on a tree rebased onto origin/main (post-ams(mcp): the AMS management tool family — governor-gated mutations, doctor, tenant health (#9523) #9607).
  • tsc --noEmit clean; mcp:tool-reference:check, mcp:client-config:check, ui:openapi:check, command-reference:check, docs:drift-check, server-manifest:check, test:mcp-pack all pass.
  • Branch coverage measured on every file this touches: client-config.ts, discovery.ts, discovery-routes.ts, discovery-route-specs.ts, and gateway.ts are at 100% statements/branches/functions/lines.
  • New suites: gateway discovery (24), gateway mount against the real stdio server in-process (6), discovery projections (33), discovery routes through the real app (16), the manifest check including its anti-rot half (19), the client-config grid (28), and the config generator (15).

@loopover-orb

loopover-orb Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Warning

⏸️ LoopOver review result - manual review recommended

Review updated: 2026-07-29 08:04:26 UTC

40 files · 1 AI reviewer · no blockers · CI green · clean

⏸️ Suggested Action - Manual Review

  • Touches a guarded path — held for manual review: This PR changes guardrail-protected path(s): .github/workflows/publish-mcp-registry.yml (matched .github/workflows/**).

Review summary
This PR adds MCP discovery surfaces (/.well-known/mcp.json + agent-tools trio), a server.json + OIDC publish workflow with an anti-rot watched-path guard, and a stdio gateway that mounts remote tools onto the local server. The code is well-factored: discovery documents are computed at request time (never committed), the client-config grid replaces four hand-written copies, and the gateway degrades gracefully on auth/network failure rather than crashing startup. The most notable risk is the gateway's tools/list discovery being unauthenticated by name collision only, relying entirely on the contract registry invariant (asserted by validate:mcp, not enforced at runtime) to prevent a duplicate-registration crash — the code does handle that gracefully as a fallback, so this is sound defense-in-depth rather than a real gap.

Nits — 8 non-blocking
  • packages/loopover-contract/src/client-config.ts:204 — the `compactStringArrays` regex nests an unbounded quantifier inside an unbounded-quantified group; since input is always self-generated JSON (never user-controlled), ReDoS risk is theoretical here, but worth simplifying regardless.
  • test/unit/mcp-gateway-mount-inprocess.test.ts:30 flagged for a possible secret-like assignment — likely a fake test token, but worth a quick check that it isn't a real credential.
  • packages/loopover-mcp/bin/loopover-mcp.ts:2560 hardcodes a 5000ms-equivalent literal (500) inline; consider naming constants like this and the FNV hash magic numbers in discovery.ts for readability.
  • The CI failures (validate-tests, validate, Workers Builds) have no detail provided; given this branch is 3 commits behind main, rebasing is worth trying before assuming a content defect.
  • Consider adding a short comment noting the registry invariant (`validate:mcp`) is what actually prevents the collision `locallyRegisteredToolNames` guards against, so a future reader doesn't mistake the guard for the primary safety mechanism.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.
  • Possible secret-shaped assignment in the diff (generic_secret_assignment) — Verify the value is not a real credential.
  • Touches a guarded path — held for manual review — A maintainer must review and merge this change.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ⚠️ Gate result — Not blocking (Advisory; not blocking this PR.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9526
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ❌ 8/20 High review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 14 registered-repo PR(s), 13 merged, 370 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 14 PR(s), 370 issue(s).
Improvement ✅ Minor risk: clean · value: minor
Linked issue satisfaction

Addressed
The PR delivers request-time discovery surfaces (/.well-known/mcp.json and agent-tools trio) with ETag/304 and per-deployment scoping, a server.json/discovery.ts implementation, a stdio gateway mode with session-gated remote tool mounting, offline advisory resource, --no-remote opt-out, transport tagging for telemetry, and a regenerated client-config grid covering all three connection modes as the

Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is registered but has no active allocation in the current snapshot.
  • Public profile languages: not available
  • Official Gittensor activity: 14 PR(s), 370 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Then work through the remaining 4 steps in the Signals table above.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask <question> answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat <question> answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

Decision record
  • action: hold · clause: guardrail_hold
  • config: 29ddea2e60a838520fc0a56f06782b59d86da32510f2cf2f1facc0d73c344b00 · pack: oss-anti-slop · ci: passed
  • record: c1a74b902d8f9f608b569d95ccf8b36f63dc63078b93e3b70edb93c59cf22d97 (schema v5, head 274be17)
Visual preview
Route Viewport Before (production) After (this PR's preview) Diff
/ desktop before /
before /
after /
after /
/ mobile before / (mobile)
before / (mobile)
after / (mobile)
after / (mobile)

Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy.

Scroll preview
Route Before (production) After (this PR's preview)
/ before / (scroll)
before / (scroll)
after / (scroll)
after / (scroll)

A short scroll-through clip (desktop) — click either thumbnail to open the full animation. Evidence for scroll-linked behavior a single screenshot can't show.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 29, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui 12e2b8e Commit Preview URL

Branch Preview URL
Jul 29 2026, 07:42 AM

@superagent-security superagent-security Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Superagent found 2 security concern(s).

Comment thread .github/workflows/publish-mcp-registry.yml
Comment thread .github/workflows/publish-mcp-registry.yml Outdated
@superagent-security superagent-security Bot added the pr:flagged PR flagged for review by security analysis. label Jul 29, 2026
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 31.51kB (0.4%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
loopover-ui 7.86MB 31.51kB (0.4%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-DsA-SAZ4.js (New) 2.16MB 2.16MB 100.0% 🚀
assets/tanstack-vendor-C4AyO3NR.js (New) 922.27kB 922.27kB 100.0% 🚀
openapi.json 16.24kB 734.69kB 2.26%
assets/docs.fumadocs-spike-api-reference-BKW71cpF.js (New) 443.45kB 443.45kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-Cnvj57m4.js (New) 201.7kB 201.7kB 100.0% 🚀
assets/modal-C6VkYPzw.js (New) 184.5kB 184.5kB 100.0% 🚀
assets/client-RQMQz9xH.js (New) 151.47kB 151.47kB 100.0% 🚀
assets/maintainer-panel-CYQDzRGK.js (New) 78.99kB 78.99kB 100.0% 🚀
assets/routes-WAxMmE6b.js (New) 35.96kB 35.96kB 100.0% 🚀
assets/owner-panel-COgC2IEC.js (New) 27.97kB 27.97kB 100.0% 🚀
assets/app-CkL34gVd.js (New) 25.78kB 25.78kB 100.0% 🚀
assets/ui-vendor-Cixa8ZAe.js (New) 24.57kB 24.57kB 100.0% 🚀
assets/miner-panel-xJCf6Uj1.js (New) 20.24kB 20.24kB 100.0% 🚀
assets/app.runs-Dv5vJwP5.js (New) 20.22kB 20.22kB 100.0% 🚀
assets/api._op-Jy5GDg9S.js (New) 17.57kB 17.57kB 100.0% 🚀
assets/self-hosting-docs-audit-C06eo0YN.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs._slug-DkvzXmkN.js (New) 15.52kB 15.52kB 100.0% 🚀
assets/mcp-clients-piMY6cFy.js (New) 14.54kB 14.54kB 100.0% 🚀
assets/playground-panel-BCiEPjJr.js (New) 14.42kB 14.42kB 100.0% 🚀
assets/fairness-BtC_01UM.js (New) 12.16kB 12.16kB 100.0% 🚀
assets/app.audit-DOi-k-Ap.js (New) 10.08kB 10.08kB 100.0% 🚀
assets/app.config-generator-D_SAwJW7.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-ClDAUvvS.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-BOvTtX5V.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-C8m0Mz5G.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/commands-panel-DfK2bC-Q.js (New) 6.65kB 6.65kB 100.0% 🚀
assets/maintainer-workflow-ORuuIfvX.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/digest-panel-DwQ-MA8r.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-CpAEJoTf.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs-nav-BUertMu-.js (New) 6.01kB 6.01kB 100.0% 🚀
assets/docs.index-D3HRC-L_.js (New) 5.95kB 5.95kB 100.0% 🚀
assets/api.index-DN4cMQis.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-Bncqus9C.js (New) 2.7kB 2.7kB 100.0% 🚀
assets/api-DE5i5T6r.js (New) 2.69kB 2.69kB 100.0% 🚀
assets/docs-page-BFwlOKfd.js (New) 2.1kB 2.1kB 100.0% 🚀
assets/table-CUQbDLei.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-ClTdKDyl.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-D_eB2wbY.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-BXwWY6xp.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-enC-L55-.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-C2cajvvz.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/app.maintainer-D2BXFehk.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-BEWEbsWC.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-BEMJKyCN.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-BsZAjnko.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-BM8_kyXH.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-D59ATuBz.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-DBJgLRF7.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner-B3TViVIZ.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-CHlhSE_-.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-D_mD-kUj.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/trash-2-*.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/save-BGcFZoqj.js (New) 327 bytes 327 bytes 100.0% 🚀
assets/git-pull-request-arrow-B1SDvIEH.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-BHawX5ax.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-CLixSxye.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-BrBQPeZD.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square-DqF8_zFx.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-CoZkn8yL.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/rotate-cw-DxmSihq9.js (New) 201 bytes 201 bytes 100.0% 🚀
assets/play-BkOoknUK.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-DPgOxyof.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/search-QhXMxK37.js (New) 174 bytes 174 bytes 100.0% 🚀
assets/add-scalar-classes-DZONjfDm.js (Deleted) -2.16MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-s4YLO76L.js (Deleted) -916.33kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-uMNOEYo9.js (Deleted) -443.45kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-D5X5QWfl.js (Deleted) -201.7kB 0 bytes -100.0% 🗑️
assets/modal-DhBTptaB.js (Deleted) -184.5kB 0 bytes -100.0% 🗑️
assets/client-vrM1QUQ-.js (Deleted) -151.47kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-CtEf8X4K.js (Deleted) -78.99kB 0 bytes -100.0% 🗑️
assets/routes-C3N9fdVV.js (Deleted) -35.96kB 0 bytes -100.0% 🗑️
assets/owner-panel-CoNjc27T.js (Deleted) -27.97kB 0 bytes -100.0% 🗑️
assets/app-D6anuP4_.js (Deleted) -25.78kB 0 bytes -100.0% 🗑️
assets/ui-vendor-DoIjjHMU.js (Deleted) -24.57kB 0 bytes -100.0% 🗑️
assets/miner-panel-oCQMDO7L.js (Deleted) -20.24kB 0 bytes -100.0% 🗑️
assets/app.runs-BtZWbpsP.js (Deleted) -20.22kB 0 bytes -100.0% 🗑️
assets/api._op-D6-WfXWl.js (Deleted) -17.57kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-BUddCv36.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs._slug-D8HkrQHG.js (Deleted) -15.52kB 0 bytes -100.0% 🗑️
assets/playground-panel-C7IC392B.js (Deleted) -14.42kB 0 bytes -100.0% 🗑️
assets/fairness-DUM_FKM7.js (Deleted) -12.16kB 0 bytes -100.0% 🗑️
assets/app.audit-VsgFiC16.js (Deleted) -10.08kB 0 bytes -100.0% 🗑️
assets/app.config-generator-CosSJkNI.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-CF2qPk8F.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-6FkILotw.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-DtJ5KCtQ.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/commands-panel-EHqfW70C.js (Deleted) -6.65kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-BNWHOPqX.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/digest-panel-CODBmjmn.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-BuSizVG7.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs-nav-Dvxhkc0i.js (Deleted) -6.01kB 0 bytes -100.0% 🗑️
assets/docs.index-Mc1jyIsE.js (Deleted) -5.95kB 0 bytes -100.0% 🗑️
assets/mcp-clients-CTylc3CB.js (Deleted) -5.2kB 0 bytes -100.0% 🗑️
assets/api.index-C_a__Z06.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-lVtEn1JX.js (Deleted) -2.7kB 0 bytes -100.0% 🗑️
assets/api-BCfnvGkA.js (Deleted) -2.69kB 0 bytes -100.0% 🗑️
assets/docs-page-MQCYFk8V.js (Deleted) -2.1kB 0 bytes -100.0% 🗑️
assets/table-Bzf4-z4N.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-CPdd7K7P.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-Qo9J3xzj.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-C8OpDE-X.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-BvS_WXsB.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-DYEh7oYZ.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-BFalmtYv.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-BbeCL0qj.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-CwrUETnY.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-DF-TwiJE.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-DP9EBBQ_.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-BFR11FrD.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-9W2g4l9-.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner-C5DlyFlX.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-ByLd4ArU.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-BsB4zXvR.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/trash-2-CipOVcb4.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/save-BaKQamSK.js (Deleted) -327 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-j3syHqZn.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-DqzHzH2m.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-DRH3DWqg.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-Bd1R_QwD.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-COPM2G8y.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-DetVN0Cp.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/rotate-cw-B_E2f4TQ.js (Deleted) -201 bytes 0 bytes -100.0% 🗑️
assets/play-CdkJq3Ia.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-BngWXlNH.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️
assets/search-pjFEWBBy.js (Deleted) -174 bytes 0 bytes -100.0% 🗑️

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.30%. Comparing base (03909f6) to head (12e2b8e).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9735      +/-   ##
==========================================
+ Coverage   90.28%   90.30%   +0.01%     
==========================================
  Files         907      912       +5     
  Lines      113357   113525     +168     
  Branches    26896    26944      +48     
==========================================
+ Hits       102342   102514     +172     
+ Misses       9683     9682       -1     
+ Partials     1332     1329       -3     
Flag Coverage Δ
backend 95.56% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/loopover-contract/src/client-config.ts 100.00% <100.00%> (ø)
packages/loopover-contract/src/discovery.ts 100.00% <100.00%> (ø)
packages/loopover-contract/src/telemetry.ts 100.00% <100.00%> (ø)
packages/loopover-mcp/bin/loopover-mcp.ts 67.25% <100.00%> (+0.82%) ⬆️
packages/loopover-mcp/lib/gateway.ts 100.00% <100.00%> (ø)
packages/loopover-mcp/lib/telemetry.ts 100.00% <100.00%> (ø)
src/api/routes.ts 95.63% <100.00%> (+0.01%) ⬆️
src/auth/route-auth.ts 100.00% <100.00%> (ø)
src/mcp/discovery-routes.ts 100.00% <100.00%> (ø)
src/mcp/server.ts 97.36% <ø> (ø)
... and 2 more

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

The server card and the agent-tools trio are COMPUTED at request time from the contract
registry. metagraphed committed its card and paid for it: every concurrent tool PR
conflicted on the same generated file. There is nothing here to regenerate and nothing a
tool PR can collide on.

The same routes serve both deployments over their own availability-filtered tool set, so a
self-host card is truthful rather than a copy of the cloud one -- a cloud-only tool is
absent because it is absent from that deployment's list, not because a second
implementation remembered to exclude it. Locality is deliberately NOT filtered: the remote
serves local-git tools too, it just expects the caller to supply the branch metadata.

generated_at derives from the version rather than the clock. A wall-clock value would
change the body on every request, changing the ETag with it and leaving the 304 path as
dead code that never fires.

serverInfo.version stops being the hardcoded "0.1.0" it has reported since the server was
written, and reads the same @loopover/mcp version the compatibility metadata already did --
so serverInfo, the card, and server.json cannot disagree about what shipped.

The manifest check carries the anti-rot guard this issue asked for: it asserts every
watched path EXISTS before validating a field. metagraphed's version-sync workflow watched
a renamed path and kept passing for months while doing nothing, and a workflow that can
quietly watch nothing is worse than no workflow.
The stdio server now discovers the remote server's tools/list with the login session and
registers anything it does not already serve, so one config gets a contributor every tool
their session entitles them to instead of one stdio config for local-git tools and a
separate remote endpoint for the rest.

The mount is best-effort by design. No session, no network, or a hostile response all leave
a server that starts and lists its local tools plus one advisory resource explaining how to
get the rest; --no-remote opts out to byte-identical pre-gateway behavior. Proxied tools
re-use the contract registry's zod schemas rather than trusting a remote-supplied schema, so
the proxy advertises exactly what the remote enforces, and carry _meta.transport="proxied"
so telemetry can tell a proxied call from a local one.

Name collisions cannot happen by construction — one registry, one entry per name, so a name
is either local-git or remote — and validate:mcp now asserts that invariant. The mount still
skips an already-registered name so a future violation degrades to "local wins" rather than
crashing the server on a duplicate registration.
…9526)

The three ways to connect -- the stdio gateway, the remote streamable-http endpoint, and the
miner's own stdio server -- were spelled out by hand in four places: clientSnippet() in the
stdio bin, the mcp-clients docs page, and both package READMEs. They had already drifted; the
docs page documented no remote auth at all, and nothing described the gateway because the
prose predates it. The grid now lives in @loopover/contract as data, `init-client --print`
renders it, and a generator writes the docs and README blocks from it under a --check drift
guard, so a snippet copied from the docs and one printed by the CLI are byte-identical.

`init-client` gains `--mode stdio|remote|miner`, defaulting to stdio so a pre-gateway
invocation prints exactly what it always did. A remote entry names the token's environment
variable in whichever dialect the host reads -- Codex's own key, the JSON hosts' header --
and never carries a value; a host whose remote dialect this repo cannot vouch for is refused
rather than given a plausible snippet that fails on paste.

Telemetry gains a `transport` dimension, so gateway adoption is measurable: `surface` says
which server was asked, and only `transport` says whether a stdio call ran against the local
checkout or was forwarded. Proxied tools now run through the same telemetry chokepoint local
ones do, and re-register with the contract's schema OBJECTS rather than their `.shape`, which
the SDK re-wraps in a way that drops the catchall and turns every extra field into a -32602.

The discovery routes are specced through the #9519 seam with real response schemas: the four
documents are now zod schemas with their TypeScript types inferred, so the published operation
describes the object actually served instead of a second declaration of it. They also report
the deployment that ANSWERED -- src/server.ts serves this same Hono app, so a self-host card
was advertising the cloud's tool set, which is a list of calls that 404 there.

Fix-what-you-find, all three the same rot: the vitest alias list for @loopover/contract, the
published-tarball allowlist, and the mount's startup cost. The first two are now derived from
the package's own exports map and its committed sources -- both had just failed by lagging a
new file, with an error naming the symptom and not the cause. The third: the mount moved after
connect() and is no longer awaited, so a network round-trip no longer sits between a client
launching the process and it answering, and registering a tool on a connected server emits
notifications/tools/list_changed -- the refresh path a stateless remote cannot push itself.
…ob (#9526)

Three defects in the registry-publish workflow, two of them raised by the security scan and all
three real.

The install step's comment said "SHA-pinned" while the code floated on a release tag, which a
maintainer can move to a different commit after review -- for a binary that authenticates as this
repository and writes to a public registry. It now verifies a sha256 of the download before
extracting it, so a moved tag fails the job rather than publishing under bytes nobody looked at.

The job holds `id-token: write` and had no deployment environment, so protection rules had nothing
to attach to and anyone able to dispatch a workflow could publish. It now declares one.

And the download would have 404'd regardless: the asset was requested as
`mcp-publisher_${VERSION}_linux_amd64.tar.gz`, but the release publishes it with no version in the
name. A publish workflow that cannot fetch its own tool is a workflow nobody has run, which is
exactly the class the anti-rot guard exists for -- so the guard now covers the workflow itself:
dispatch-only, main-only, environment present, permissions minimal, actions sha-pinned, the
checksum verified before extraction, and the manifest check ordered before login and publish.

Also fixes a genuine flake this PR's CI hit twice, unrelated to the change: selfhost-pg-queue's
recentDeadCount assertion compared the bound cutoff only against a timestamp captured BEFORE the
call, while the implementation reads the clock after it -- so one millisecond ticking between the
two failed the test (1785302335173 <= 1785302335172). Bracketed on both sides now.
Raising the gateway's patch coverage found a real defect in it. registerProxiedTool declared an
inputSchema only for a tool the contract registry knows, and the SDK invokes a handler as
`(args, extra)` ONLY when an inputSchema is declared -- as `(extra)` alone when it is not. So a
proxied tool absent from the registry, which is precisely the case the fallback exists for (a
remote running ahead of this package), forwarded the SDK's own `{ signal, requestId }` to the
remote AS THE ARGUMENTS and silently dropped everything the caller passed.

An unmodelled tool now gets a fully open input schema instead of none, which keeps the remote the
only validator -- where validation belongs for a tool this package does not model -- while making
the SDK route arguments the way it does for every other tool.

The test that caught it was previously asserting only that the call resolved. callTool resolves
for a FAILED call too, so it passed while nothing was being proxied at all: the fixture had no
/mcp route, the proxy 404'd, and the SDK turned the throw into an isError result nobody checked.
The fixture now answers tools/call with a real JSON-RPC envelope and the assertion is on the
payload -- the tool name and the arguments as the remote received them.

Also: the advisory resource is registered on every outcome rather than only on failure, so a
successful mount no longer leaves an earlier failure's advisory standing as the answer to "why
don't I have the remote tools". Plus coverage for the paths that had none -- calling a proxied
tool, reading the advisory, a descriptor carrying annotations, one carrying only a name, an
envelope with no `result`, the real fetch transport against the loopback fixture, and the
no-session path that makes no outbound request at all.
The five branch partials codecov/patch was holding the PR on, all in init-client's argument
handling: no host at all, an unknown host, the --client spelling as well as --print, a
host/mode pair the grid refuses, and the per-host remote caveat's absent side.

Each asserts the message rather than merely the throw -- "unsupported client" without the list
leaves a reader guessing at a five-value set they cannot see from outside the process.
@JSONbored
JSONbored merged commit 983ea31 into main Jul 29, 2026
11 checks passed
@JSONbored
JSONbored deleted the feat/mcp-discovery-gateway-9526 branch July 29, 2026 08:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

manual-review Gittensor contributor context

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mcp(discovery): server.json + .well-known surfaces, MCP registry publish, and the stdio gateway front door

1 participant