Skip to content

PAGS has no check on what tools/list publishes — adopt the spec schema plus the directory bar, and skip the official conformance suite (it cannot authenticate and grades SDKs) #562

Description

@serge-ivo

The gaps in #561 were found by a person reading someone else's settings page

Nothing PAGS runs would have found them. There is no check anywhere in the repo that looks at what
tools/list actually publishes, so 135 tools shipped with no annotations, no titles and no output
schemas, and the build stayed green throughout. The question this issue answers is not "what is
wrong with the tool surface" (#561 owns that) but "what does PAGS run so the next one fails a
build instead of a user."

I went looking for a published gold standard. One exists, it is first-party, and it is not the
right tool for PAGS
— and the reason why is the finding that decides this issue.


What exists today, from primary sources

1. @modelcontextprotocol/inspector — first-party, and it is now genuinely scriptable

Still first-party and actively maintained (repo,
last push 2026-08-12). Since my last look it has grown a documented CLI with stable exit codes
and CI recipes (docs):

Each CLI run connects to a server, invokes the single request you name with --method, prints the
result, and exits.

Code Meaning
3 Server requires authentication (401/403, WWW-Authenticate, OAuth).
4 Server unreachable.
5 Tool error: tools/call returned isError: true, or the tool wasn't found.

and, for non-interactive runs:

--stored-auth-only: never start interactive OAuth or step-up… Use tokens from the shared store if
present, otherwise fail immediately with auth_required. This is the flag CI wants.

But it validates nothing. It invokes and prints; the documented CI recipe is a jq -e assertion
the caller writes:

mcp-inspector --cli --config ./ci-servers.json --server my-server \
  --stored-auth-only --method tools/list --format json \
  | jq -e '.result.tools | map(.name) | index("get_weather")' > /dev/null

So the Inspector is a reachability and presence probe, not a conformance checker. It is the only
one of the four things here that can talk to mcp.proagentstore.online as it is deployed.

2. There IS a machine-checkable artefact, and a first-party conformance suite

The schema. schema/<revision>/schema.json is published for every revision
(spec repo).
Measured today:

Revision $schema defs Tool.required
2025-06-18 draft-07 91 ["inputSchema","name"]
2025-11-25 2020-12 145 ["inputSchema","name"]
2026-07-28 (exists; our SDK cannot speak it, see below)

The suite. modelcontextprotocol/conformance
"Conformance Tests for MCP", first-party, MIT, 101 stars, created 2025-07-10, last push
2026-08-10. Published as @modelcontextprotocol/conformance (latest stable 0.1.16, 2026-03-30;
0.2.0-alpha.11 on 2026-08-07 — pre-1.0). It ships a composite GitHub Action, an
expected-failures baseline with regression and stale-baseline exit codes, and per-revision
requirement sets (requirements/2025-11-25.yaml, requirements/2026-07-28.yaml).

It also does the thing you would want most:

Every scenario also validates each JSON-RPC message on the wire against the spec's JSON schema for
the negotiated spec version… wire-schema-valid — fails when a message the implementation under
test sent
violates the spec JSON schema.

I ran it against production. Measured, 2026-08-14:

$ npx @modelcontextprotocol/conformance@latest server \
    --url https://mcp.proagentstore.online/mcp --scenario server-initialize

[server-initialize] FAILURE Server responds to initialize request with valid structure
  Error: Failed to initialize: Streamable HTTP error: Error POSTing to endpoint:
         {"error":"invalid_token","error_description":"Missing or invalid access token"}
Passed: 0/1, 1 failed

Confirmed against the server directly — an unauthenticated initialize returns
HTTP 401 with WWW-Authenticate: Bearer realm="OAuth", …, which is correct behaviour from
OAuthProvider({ apiRoute: "/mcp" }) (workers/mcp/src/index.ts:1143-1147).

The server subcommand has no way to authenticate. Read from
src/index.ts:529-558:
--url (required), --scenario, --suite, --expected-failures, -o, --spec-version,
--force, --requirements, --verbose. No --header, no --token, no OAuth. Server-side auth
testing is open work upstream:
#64,
#105,
#106 — all open.

And most of what it requires, PAGS does not implement and should not. The required server list
in requirements/2025-11-25.yaml
has 31 entries. It includes resources-list, resources-read-text, resources-subscribe,
prompts-list, prompts-get-with-args, logging-set-level, completion-complete,
tools-call-sampling, tools-call-elicitation — features #561 already recorded as a deliberate
non-goal. Worse, the tool scenarios expect a reference fixture, not a product:
src/scenarios/server/tools.ts requires the server under test to implement a tool literally named
test_simple_text returning an exact payload; stateless wants test_missing_capability.

That is the answer to "is this our gold standard": no. It is the conformance suite for
SDKs — its own tier-check subcommand grades SDK repositories against
SEP-1730. Pointed at a
product server it would score roughly 4/31 and the number would mean nothing.

3. The real bar is the two directories — and it is stricter than the spec

Anthropic, whose MCP Directory Policy has been consolidated into the
Anthropic Software Directory Policy
(the old URL now
just points at it). §5, Additional Requirements for Model Context Protocol Servers, quoted verbatim
from the page:

A. MCP servers must gracefully handle errors and provide helpful feedback rather than generic error messages.
B. MCP servers must be frugal with their use of tokens. The amount of tokens a given tool call uses should be roughly commensurate with the complexity or impact of the task…
C. MCP tool names must not exceed 64 characters.
D. Remote MCP servers that connect to a remote service and require authentication must use secure OAuth 2.0 with certificates from recognized authorities.
E. MCP servers must provide all applicable annotations for their tools, in particular readOnlyHint, destructiveHint, and title.
F. Remote MCP servers should support the Streamable HTTP transport…

Plus, from §3: a standard testing account with sample data, and "at least three working examples of
prompts or use cases"
.

Item E is a hard listing requirement and it names the exact three fields #561 is adding
including title, which #561 currently carries as an "adjacent gap, fold in or reject". This
upgrades it: it is a directory bar, not legibility.

OpenAI, App review — the submission
scan imports "tool names, titles, and descriptions; input and output schemas; security schemes;
_meta fields; tool annotations; linked UI resource metadata…; and MCP server instructions"
, and
the review verifies "tool hint annotations accurately reflect behavior".
App guidelines: "Incorrect or missing
action labels are a common cause of rejection.
"
(Already established on
#561's comment
not re-derived here.)

4. Multi-host: no published "test against N hosts" rule that I could find

I looked and did not find one. What is published is a
client extension support matrix — it
covers only the three official extensions (MCP Apps, OAuth Client Credentials, Enterprise-Managed
Authorization), not core features, and it is explicitly "maintained by the community". It does not
tell you which host exercises resources vs. prompts vs. sampling.

Since PAGS ships no resources, no prompts, and requires no sampling or elicitation (#561), the
features a second host would exercise that ChatGPT does not are close to nil. My recommendation is
to not chase multi-host coverage
, and to treat "does it work in Claude Desktop / Claude Code" as a
manual check performed once, at the point of a directory submission. Recorded as a negative result
rather than left as an open question.


The measurement that decides the design

I built the real thing in a scratch spike: a real McpServer (SDK 1.29.0, the pinned version), tools
registered through the exact call shape PAGS usesserver.tool(name, desc, zodShape, handler)
connected to a real Client over InMemoryTransport, then client.listTools(), then Ajv against the
spec's Tool definition.

Emitted wire object:

{
  "name": "list_agents",
  "description": "List published agents",
  "inputSchema": { "type": "object", "properties": { "limit": { "type": "number" } },
                   "additionalProperties": false,
                   "$schema": "http://json-schema.org/draft-07/schema#" },
  "execution": { "taskSupport": "forbidden" }
}

Result:

[2025-06-18] Tool-schema violations: 0
[2025-11-25] Tool-schema violations: 0
annotations present: 0/2 · titles: 0/2 · outputSchema: 0/2

A spec-schema conformance check passes cleanly on exactly the defect #561 found. Tool.required
is ["inputSchema","name"]description, title, annotations and outputSchema are all
optional. So schema validation is real and worth having, but it is not the check that would have
caught today, and an issue that recommended it as the answer would be recommending a guard that
certifies ground it never walked (ADR 0002).

Two layers, and they must be labelled differently:

  • Spec conformance — "is this structurally a legal MCP tool surface?" Cheap, catches future
    breakage, would have caught nothing today.
  • Directory conformance — "does this meet the published bar for listing?" This is the layer with
    today's findings in it, and no published validator enforces it. PAGS has to assert it, and
    PAGS is unusually well placed to, because safety.ts already classifies every tool.

What to do, cheapest first

The hook is workers/mcp/src/index.test.ts. Its harness constructs the real PagsMcp, replaces
this.server with a capturing double (index.test.ts:122-127) and runs the real init(); the
suspension test at index.test.ts:496 asserts h.tools.size === MCP_TOOL_COUNT before iterating —
ADR 0002 names it "the hardest thing in this repo to fool". Verified that only this.server.tool
is ever called on that object (35 call sites in index.ts, plus the delegated groups), so the double
can be swapped for a real McpServer without touching production code.

Step 1 — a tools/list guard built on the REAL emitted objects (do this)

New file workers/mcp/src/conformance.test.ts. Same harness shape as index.test.ts, but mock only
agents/mcp (not @modelcontextprotocol/sdk/server/mcp.js), assign a real McpServer, connect
an InMemoryTransport pair to a real Client, and call listTools(). That yields the byte-for-byte
Tool[] a host receives. Proven to work in the spike above.

Assert, over the whole surface with every group gated on:

  1. tools.length === MCP_TOOL_COUNT — ADR 0002 G1, and it reuses the constant that already
    exists precisely because that number rotted once (tool-count.ts:23).
  2. Every name is SEP-986 legal: 1–64 chars, ^[A-Za-z0-9_./-]+$. This is simultaneously the
    spec's rule and Anthropic §5.C. Measured today by a source scan: 135 distinct names, 0
    violations, max length 31 — so this arm goes green on day one and Step 4 below matters.
  3. Every tool has a non-empty description. Not required by Tool.required, but required by the
    conformance suite's own tools-list scenario and by Anthropic §2. Measured: 135/135 have one.
  4. Every tool carries annotations.readOnlyHint, annotations.destructiveHint, and a title
    Anthropic §5.E verbatim. This arm is red until The MCP server never publishes tool annotations or output schemas — it already classifies every tool read/write/destructive and tells no host #561 lands, which is the point: it converts
    The MCP server never publishes tool annotations or output schemas — it already classifies every tool read/write/destructive and tells no host #561 from a fix into an invariant.
  5. Annotation ⇄ scope agreement. destructive scope ⇒ destructiveHint: true; read/none
    readOnlyHint: true; write/runtimereadOnlyHint: false; a contradiction fails. This is
    the check no external tool can perform, because only PAGS knows safety.ts is the authority.
    The MCP server never publishes tool annotations or output schemas — it already classifies every tool read/write/destructive and tells no host #561's comment already worked out the shape (and the remove_repo exception, whose scope depends
    on its arguments). Implement it there; this issue just requires it be asserted here, on the whole
    registered surface rather than the ~90 instance tools contract.test.ts covers.
  6. Print the denominator — ADR 0002 G2. e.g.
    ✓ 135 tools: 135 annotated, 135 titled, 12 with outputSchema, 0 SEP-986 name violations.

Cost: one test file, no new production code, no new CI step —
workers/*/src/**/*.test.ts is already in the UNIT_TESTS glob (vitest.config.ts), so pnpm test
picks it up.

Step 2 — validate the same array against the published spec schema

On the same Tool[], plus the whole ListToolsResult. Vendor
schema/2025-11-25/schema.json into workers/mcp/src/ (the upstream conformance suite vendors the
same files at src/spec-types/*.schema.json for the same reason: CI must not depend on the network),
validate $defs/Tool with Ajv2020 + ajv-formats.

Target 2025-11-25 — verified as the newest revision the pinned SDK speaks
(@modelcontextprotocol/sdk@1.29.0 dist/esm/types.js:2-3: LATEST_PROTOCOL_VERSION = '2025-11-25',
SUPPORTED_PROTOCOL_VERSIONS = [LATEST, '2025-06-18', '2025-03-26', '2024-11-05', '2024-10-07']).
2026-07-28 exists upstream but the SDK cannot negotiate it; validating against it would measure a
protocol PAGS does not speak.

ajv is not a direct dependency anywhere in the monorepo today, but is already resolved in the store
as a dep of the MCP SDK (ajv ^8.17.1, ajv-formats ^3.0.1) — add it as a devDependency on
workers/mcp.

Cost: ~30 lines on Step 1's harness plus one vendored JSON file. Buys future protection for the
hand-written outputSchemas #561 introduces — a malformed one currently fails only at runtime, inside
the SDK's validateToolOutput.

State plainly, in the test header, that this arm passed before #561 and therefore does not cover it.

Step 3 — do NOT wire the official conformance suite into CI

Recommended against, on the evidence above: it cannot authenticate (measured 401; upstream #64/#105/#106
open), it is pre-1.0, and 26 of its 31 required server scenarios test features PAGS deliberately does
not implement or fixture tools it will never have. A red build carrying a meaningless number is worse
than no build step.

There is a generic subset worth one bounded spike, if and only if standing up a local
unauthenticated instance is cheap: server-initialize, tools-list, ping, server-session-lifecycle,
http-header-validation, dns-rebinding-protection. The first three duplicate Step 1 in a weaker form.
The last two are the only genuinely new information, and they test HTTP-layer behaviour owned by
@cloudflare/workers-oauth-provider and agents/mcp, not by PAGS code. Timebox it to an hour; if a
local unauthenticated entry point is not already trivially available, drop it
— do not add a
production entry that bypasses OAuth in order to run a test. Re-evaluate when upstream #64 lands.

Step 4 — prove each arm fails (ADR 0002 G4)

Non-optional, and the reason arms 2 and 3 of Step 1 are worth writing at all despite going green
immediately. Before merge, for each arm: rename one tool to contain a :; blank one description;
delete one tool's annotations; flip cancel_instance's destructiveHint to false; add a 136th
tool without touching MCP_TOOL_COUNT. Record in the PR that each flipped the assertion red.

Optional, after the above — Inspector CLI as a post-deploy smoke check

mcp-inspector --cli https://mcp.proagentstore.online/mcp --transport http --stored-auth-only --method tools/list --format json | jq -e '.result.tools | length == 135'. It is the only tool here
that can reach the deployed server. Not in CI--stored-auth-only needs a token in the shared
store, and putting a long-lived OAuth token in CI to assert a number Step 1 already asserts is a poor
trade. Worth documenting in platform-docs/mcp.md as the manual "is prod actually serving what main
says it serves" command, and worth using the exit codes (3 auth, 4 unreachable) if a scheduled
uptime probe is ever wanted.


Alternatives considered and rejected


Acceptance criteria

  1. workers/mcp/src/conformance.test.ts obtains its Tool[] from a real McpServer +
    InMemoryTransport + real Client.listTools() driven by the real PagsMcp.init() — not from a
    reconstruction of what the SDK would emit.
  2. It asserts tools.length === MCP_TOOL_COUNT before iterating, and its passing output states that
    number alongside the per-property tallies and the spec revision validated against (ADR 0002 G1+G2).
  3. Every one of the 135 tools is asserted for: SEP-986 name legality, non-empty description,
    annotations.readOnlyHint + annotations.destructiveHint + title present, and agreement between
    annotation and safety.ts scope.
  4. The same array validates against the vendored schema/2025-11-25/schema.json $defs/Tool, and the
    full response against ListToolsResult. Any tool whose validation errors (as opposed to fails)
    is counted and asserted zero, never skipped (ADR 0002 G3).
  5. The test header states, in words, what it does and does not measure — specifically that spec-schema
    validation alone passed before The MCP server never publishes tool annotations or output schemas — it already classifies every tool read/write/destructive and tells no host #561 and does not cover annotations, titles or output schemas.
  6. Each arm has been watched to fail; the PR records how (ADR 0002 G4).
  7. No production code changes in workers/mcp/src/ outside what The MCP server never publishes tool annotations or output schemas — it already classifies every tool read/write/destructive and tells no host #561 already requires. In particular,
    no unauthenticated entry point is added.

Regression risk

Open question for the owner

Is a directory listing an actual goal? Steps 1–2 are worth doing either way — annotations improve
host behaviour whether or not anyone reviews them. But if listing in Anthropic's Software Directory or
OpenAI's app directory is a real objective, then §3 of the Anthropic policy adds work this issue does
not cover: a standard testing account with sample data, a privacy-policy link, and three worked example
prompts. I would say yes, and treat #561 + this issue as its first two steps — the annotation
requirement is a bar PAGS has to clear regardless, and clearing it deliberately is cheaper than
clearing it under review feedback. Filed as a question rather than assumed.


Verified vs. inferred

  • Verified (measured today, 2026-08-14/15): the production 401 and the conformance CLI's failure
    against it; the conformance CLI's complete server-mode option list (no auth flag); the 31-entry
    required server set and its fixture-tool dependency; the two spec schemas' Tool.required; the spike
    showing PAGS's registration shape emits Tool objects that validate at both revisions with 0
    annotations/titles/outputSchemas; 135 tool names scanned with 0 SEP-986 violations, max length 31;
    135/135 non-empty descriptions; LATEST_PROTOCOL_VERSION = '2025-11-25' in the pinned SDK;
    InMemoryTransport present in the installed SDK; this.server.tool being the only method PAGS calls
    on the server object; workers/*/src/**/*.test.ts already in the vitest unit glob.
  • Verified (external, quoted): the Inspector CLI's exit codes, --stored-auth-only and CI recipe;
    the conformance suite's wire-schema checks, baseline exit-code table and requirement-set semantics;
    Anthropic Software Directory Policy §5 A–G verbatim; OpenAI's app-review scan inventory.
  • Inferred: that Steps 1–2 would have caught a future recurrence of The MCP server never publishes tool annotations or output schemas — it already classifies every tool read/write/destructive and tells no host #561's class. They would have
    caught the annotation class by construction; I have not run them against a planted defect, which is
    exactly what Step 4 exists to fix before merge.
  • Not established: whether any host beyond ChatGPT currently reads PAGS's tool surface at all — I
    did not test Claude Desktop, Claude Code or Cursor against mcp.proagentstore.online, and did not
    find published guidance requiring it. Recorded as a non-reproduction, not a gap.

Related: #561 (the gaps themselves — do not duplicate), ADR 0002 (docs/adr/0002-a-guard-states-what-it-measured.md).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions