Skip to content

Describe the surface in model-readable metadata, don't direct the model - #167

Open
aterga wants to merge 6 commits into
claude/icp-marketplace-compliance-vpawel-7-finance-canister-disallowfrom
claude/icp-marketplace-compliance-vpawel-10-simplify-metadata
Open

Describe the surface in model-readable metadata, don't direct the model#167
aterga wants to merge 6 commits into
claude/icp-marketplace-compliance-vpawel-7-finance-canister-disallowfrom
claude/icp-marketplace-compliance-vpawel-10-simplify-metadata

Conversation

@aterga

@aterga aterga commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

The server instructions and the served tool descriptions told a model how to work: START WITH open_app, ALWAYS call this FIRST, NEVER guess a domain, "web search the official URL", a numbered seven-step flow across tools, and a routing chain to run before answering anything. Both directories read these fields — OpenAI scans server instructions and prohibits model-readable fields that steer how the model works, and descriptions that tell Claude how to behave are a rejection trigger — so every surface now describes what it is and how it behaves instead.

Nothing a caller needs was dropped; it is restated as fact rather than instruction. "An anonymous OQL read is rejected" and "a URL with no Internet Computer evidence is refused" tell a model everything the old imperatives did, without telling it what to do.

Stacked on #163 (base is that branch, so the diff shows this change alone; GitHub retargets to main when #163 merges). It rewrites the same regions #163 just touched — the update-call description and the instructions' policy paragraph — so branching off main would have lost that work.

Related issues

None — follow-up in the directory-submission series.

Changes

  • Server instructions: 10005 → 3521 characters. What remains is a description: the value encoding, what the two tool families act on, what a derivation origin is and which reads it gates, the two Internet Identity access levels, how canister values are stored, the financial-transactions policy, and where building and deploying happen. Gone: the START WITH entry-point rule, the routing chain, the numbered "Typical flow", the locale-conversion directives, "web search or ask the user", and the caps imperatives. The policy paragraph keeps its wording (and its pinned phrases) from Refuse update calls to known finance-related canisters #163.
  • All 11 served descriptions rewritten as capability statements — what the tool does, what it returns, what it rejects. canister_query loses its "start with open_app first" preamble; open_app loses its ROUTING block; resolve_app and discover_app_canisters lose the "NEVER guess / web search" rules and keep the fact that a derived domain is not resolvable. canister_update_call keeps the refusal disclosure added in Refuse update calls to known finance-related canisters #163.
  • Argument schemas follow the same rule (calls.rs, discover.rs, identities.rs): NOT/NEVER/REQUIRED imperatives become plain statements, and "or ask the user for it" becomes what actually provides the value.
  • New test model_readable_metadata_states_capabilities_not_directives checks the instructions and all 11 descriptions for the phrasings that crept in before, so they cannot come back.
  • Submission docs record the posture: the OpenAI readiness table gains rows for the two guideline rules this answers ("tools should behave exactly as their names, descriptions, and inputs indicate" and the model-readable-fields rule), and the Anthropic doc notes the same in the tool-surface section. One stale test-case line — the instructions "prime the assistant" to recommend reconnecting — now says what the instructions actually do.

Out of scope: the 15 deferred IcProtocolTools descriptions are not served and are unchanged.

Testing

  • cargo build --locked --all-targets
  • cargo test --locked --all-targets — 154 core (up from 153) + 52/10/9/20/1, all green
  • cargo clippy --all-targets — unchanged 10-warning baseline (this repo is not rustfmt-clean; no repo-wide cargo fmt, matching prior PRs)
  • npm test --prefix monitoring/mcp-status (dashboard unchanged)

Checklist

  • I have read the Contributing guidelines.
  • Docs (README / comments) updated for any user-visible change.
  • No secrets, credentials, or internal-only information are included.

Generated by Claude Code

claude added 2 commits August 28, 2026 10:55
The server instructions and the 11 served tool descriptions told a model
how to work — "START WITH open_app", "ALWAYS call this FIRST", "NEVER
guess a domain", "web search the official URL", a numbered seven-step
flow across tools, and a routing chain to run before answering. Both
directories read these fields: OpenAI scans server instructions and
prohibits model-readable fields that steer how the model works, and
descriptions that tell Claude how to behave are a rejection trigger.

Every surface now states what it is and how it behaves. The instructions
go from 10005 to 3521 characters and cover the value encoding, what the
two tool families act on, what a derivation origin is and which reads it
gates, the two Internet Identity access levels, how canister values are
stored, the financial-transactions policy, and where building and
deploying happen. Each description says what its tool does, what it
returns, and what it rejects. Behavior that a caller needs stays, stated
as fact rather than instruction: an anonymous OQL read is rejected, a
Candid data query on an OQL canister is rejected, a URL with no Internet
Computer evidence is refused. Argument schemas follow the same rule.

A unit test pins the property across the instructions and all 11
descriptions, so the phrasings that crept in before cannot come back.
The submission docs record the posture against the guidelines each
directory publishes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
Keeps this stacked branch current with its base (#163), so the metadata
diff shows only its own changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk

aterga commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

CI is red on aa71657, and it is not this PR's failure.

The failing check is CI / test, on one live-network test:

discover::tests::search_finds_ckusdc_and_lookup_identifies_it
lookup: "dashboard request failed: error sending request for url
(https://ic-api.internetcomputer.org/api/v3/canisters/xevnm-gaaaa-aaaar-qafnq-cai)"

The IC dashboard API is down — that URL returns HTTP 524 and then times out on three consecutive direct attempts from here. This PR changes tool descriptions, the server instructions, and argument-schema documentation; it changes no request path. The same test failed at the same time on #164, an unrelated diff, and this branch's own previous head (30850a0) passed CI at 10:56Z, before the outage.

Nothing to port and nothing to weaken: the test asserts against a live service, and relaxing it to survive an outage would cost the assertion its value. The re-run waits for the dashboard; a check-in is scheduled and will report back here only if the result is anything other than green.

For the record, the head also carries a merge of this PR's base branch (#163's c90baaf), so the diff shown here stays limited to the metadata change.


Generated by Claude Code

@aterga
aterga requested a balanced review from Copilot August 28, 2026 13:43
@aterga
aterga marked this pull request as ready for review August 28, 2026 13:44
@aterga
aterga requested a review from a team as a code owner August 28, 2026 13:44
Comment thread crates/imcp2-core/src/tools.rs Outdated

#[tool(
description = "Make an UPDATE call (a state-changing call) on an Internet Computer canister method, with textual Candid in and out. Financial operations and update calls to known financial-service canisters are not supported and return an error. Args are encoded against the method's declared Candid types (so plain literals like 42 coerce correctly — no `: type` annotations needed). Omit `derivation_origin` to call anonymously, or pass it to call AS your account at that app — a short-lived account delegation is derived on demand from this connection's standing Internet Identity credential. `derivation_origin` is the app's EXACT canonical II derivation origin (not necessarily its visible URL; don't infer it from alternativeOrigins). Get it once from open_app / resolve_app (which turn an app name or URL into the derivation origin under the guessed-domain gate) and reuse it here — this tool does NOT accept a raw website URL. By default this uses the app's default account; pass `account` (a name from list_app_accounts) for a specific one. The result echoes `derived_for_origin` + `requested` + `acted_as_principal` so you can catch an origin mismatch. For READ-only calls (Candid query methods or OQL queries) use canister_query instead. If get_canister_candid couldn't fetch the interface, pass the `.did` text as `candid` so args/replies are still typed.",
description = "Make an update call (a state-changing call) on an Internet Computer canister method, with textual Candid in and out. Financial operations and update calls to known financial-service canisters are not supported and return an error. Args are encoded against the method's declared Candid types, so plain literals like 42 coerce correctly without `: type` annotations. Omitting `derivation_origin` calls anonymously; passing it calls as the user's account at that app, with a short-lived account delegation derived on demand from this connection's standing Internet Identity credential. `derivation_origin` is the app's exact canonical Internet Identity derivation origin — not necessarily its visible URL, and not an alternative-origins entry — which open_app and resolve_app resolve from an app name or URL; this tool takes the origin itself, not a raw website URL. `account` names one of the user's accounts (list_app_accounts returns them); omitted, the app's default account is used. The result echoes `derived_for_origin`, `requested`, and `acted_as_principal`, so an origin mismatch is visible. Read-only calls — Candid query methods and OQL queries — go through canister_query. `candid` supplies the interface as `.did` text when the canister's own metadata can't be read, so args and replies stay typed.",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yet again it says "Financial operations and update calls to known financial-service canisters are not supported and return an error." is here, this should be omitted! It's not up for discussion, @aterga also agrees that tools should not mention financial in any sort of way in their descriptions!.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It's already gone — your review raced the push by about a minute. The sentence came out of #163 in 2fe9274, and this branch (stacked on #163) picked it up in the merge 990e685, which resolved this exact hunk: the description here keeps its factual text minus that sentence. grep -c "Financial operations and update calls" crates/imcp2-core/src/tools.rs returns 0 on the current head.

Nothing about it is up for discussion from my side either — I raised it on #163 rather than pushing my own answer, precisely because it was your rule against @aterga's request, and @aterga settled it your way (drop it entirely rather than reword it).

It is also now enforced rather than merely done, in two layers: financial_policy_is_a_server_instruction_not_a_description asserts that no served tool description contains "financial", and as of 10c2f3d the directive scan covers every tool's input and output schemas too — a bot review found that a field doc had slipped a "never infer" past the description-only scan.


Generated by Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Reframes MCP metadata as factual capability descriptions rather than model directives.

Changes:

  • Rewrites server, tool, and argument descriptions.
  • Adds a regression test for directive wording.
  • Updates directory-submission documentation.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
docs/openai-directory-submission.md Documents OpenAI metadata compliance.
docs/anthropic-directory-submission.md Documents Anthropic metadata posture.
crates/imcp2-core/src/tools.rs Rewrites served metadata and adds testing.
crates/imcp2-core/src/identities.rs Revises resolver argument metadata.
crates/imcp2-core/src/discover.rs Revises discovery argument metadata.
crates/imcp2-core/src/calls.rs Revises call argument metadata.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/openai-directory-submission.md Outdated
| No machine-to-machine grants (client credentials etc. unsupported by ChatGPT) | ✅ user-consent authorization-code flow only |
| Tools explicitly annotated `readOnlyHint` / `destructiveHint` / `openWorldHint` — "incorrect or missing action labels are a common cause of rejection" | ✅ set on all 11 tools. The unit test enforces annotation presence and the `readOnlyHint`/`destructiveHint` values; `openWorldHint` is declared everywhere but not asserted by the test, so re-check it in the portal's Scan Tools step |
| Tool names "human-readable, specific, and descriptive"; accurate descriptions; minimum-information requests | ✅ reviewed against the same bar for the Anthropic listing |
| "Tools should behave exactly as their names, descriptions, and inputs indicate"; "side effects should never be hidden or implicit" | ✅ each description states what the tool does, returns, and rejects — `canister_update_call`'s names its refusal of financial operations and known financial-service canisters |

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 10c2f3d — and the sentence needed rewriting anyway, not just repair: it claimed canister_update_call's description names its financial refusal, which stopped being true when that sentence was removed. The row now reads that each description states what the tool does, returns, and rejects, with the financial-transactions policy stated in the server-level instructions rather than in any description.


Generated by Claude Code

Comment thread crates/imcp2-core/src/identities.rs Outdated
Comment on lines +454 to +456
/// name, or from the app's official site. A domain derived from an app's name
/// is not resolvable here: lookalike domains are unrelated or squatted sites
/// and are refused.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 10c2f3d. The field now describes the check that runs: when the derivation origin has to be assumed from the URL, the origin is checked for Internet-Computer evidence (a gateway x-ic-canister-id) and the call is refused without it — so a lookalike that is not an IC app does not resolve, stated as the mechanism rather than as a rule about where the URL came from.


Generated by Claude Code

Comment thread crates/imcp2-core/src/discover.rs Outdated
Comment on lines +1822 to +1824
/// given. A domain derived from a name rather than known is not resolvable
/// here: an unknown bare name is refused with instructions for finding the
/// real URL, and a URL with no Internet-Computer evidence is refused.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Correct, and it's the same class of error the PR set out to remove: a claim about the caller's provenance, which the server has no way to observe. Fixed in 10c2f3d.

OpenAppArgs.app now states the two refusals that actually happen — an unknown bare name is refused with instructions for finding the real URL, and a URL whose origin shows no Internet-Computer evidence is refused — and drops the "a domain derived from a name is not resolvable here" clause, which was unenforceable: an explicit URL is resolved as given, whatever the caller derived it from.

ResolveAppArgs.app_url (your other comment) had the same claim and got the same treatment.


Generated by Claude Code

Comment thread crates/imcp2-core/src/tools.rs Outdated

#[tool(
description = "Discover the Internet Computer canisters behind a web domain (e.g. \"opencloud.org\"). The domain must be one you actually have (from the user, open_app's known-app resolution, or a web search) — NEVER a domain guessed from an app's name; when you only know a NAME, call open_app with the name first. Returns every canister id found, with provenance, most authoritative first: app-declared metadata — the App Connect page's `ic:canister-id` meta at /ai-connect.html (the app's MAIN backend) and the app's own /.well-known/ic-app.json manifest (ALL its canisters, with roles) — then the `x-ic-canister-id` header (the frontend/asset canister), a `/env.json` runtime config (e.g. backend_canister_id), and labelled/bare canister-id literals mined from the JS bundle. App-declared entries are the app's own claim about itself; env.json/bundle entries are mined candidates: pick by label (prefer production/IC ids) and confirm with get_canister_candid before calling.",
description = "Discover the Internet Computer canisters behind a web domain (e.g. \"opencloud.org\"). `domain` is a domain the caller already has — from the user, from open_app's known-app resolution, or from the app's official site — rather than one derived from an app's name; open_app takes a name directly. Returns every canister id found, with provenance, most authoritative first: app-declared metadata — the App Connect page's `ic:canister-id` meta at /ai-connect.html (the app's main backend) and the app's own /.well-known/ic-app.json manifest (all its canisters, with roles) — then the `x-ic-canister-id` header (the frontend/asset canister), an `/env.json` runtime config (e.g. `backend_canister_id`), and labelled or bare canister-id literals mined from the JS bundle. App-declared entries are the app's own claim about itself; env.json and bundle entries are mined candidates, distinguished by label (production and IC ids) and confirmable with get_canister_candid.",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Verified and fixed in 10c2f3d. bound_findings caps results at 50 ids, and unlabelled bundle literals at 20 before that, folding everything dropped into omitted — so "every canister id found" was a promise the tool doesn't keep on a big app.

The description now says it returns up to 50 ids, notes the tighter cap on unlabelled bundle literals, and says any id dropped by those bounds is counted in omitted rather than left out silently. The same edit removed the provenance clause from that description, per your other finding.


Generated by Claude Code

Comment on lines +2578 to +2582
for tool in super::IcTools::all_tools() {
surfaces.push((
tool.name.to_string(),
tool.description.as_deref().unwrap_or_default().to_string(),
));

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is the finding that mattered most in the round — the test was checking the surface it was cheapest to check, not the surface the rule covers. Fixed in 10c2f3d.

The scan now includes every served tool's serialized input_schema and output_schema alongside its description and the server instructions. A field doc reaches the model exactly like a description does, so there was no principled reason to exclude them.

And you're right that there was a live violation behind the hole: OpenAppOutput.alternative_origins said "never infer the derivation origin from it". It now states the same thing as a fact — an entry there is an origin that may derive from the derivation origin, the inverse relation — with no instruction to the model.

One addition beyond the ask: the test now asserts the scan actually sees field documentation (open_app's output schema must contain a known phrase). Without that, a serialization change that stopped carrying description keys would make the new coverage silently vacuous and the test would still pass — which is the same failure mode as the original hole.


Generated by Claude Code

claude added 4 commits August 28, 2026 13:52
Resolves two conflicts in tools.rs against this branch's metadata work:

- canister_update_call keeps this branch's factual description, minus
  the financial sentence #163 has now removed from every description.
- The two instructions-only tool tests go with the tools they covered;
  this branch's model_readable_metadata_states_capabilities_not_directives
  test stays, and now runs over the 11-tool served surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
The directives test read tool descriptions and the server instructions
but not the schemas — and a directive in an argument or reply field's doc
comment reaches the model exactly like one in a description. Review found
the hole and a live violation through it: OpenAppOutput's
alternative_origins said "never infer". The scan now covers every served
tool's input and output schema, that field states the inverse relation as
a fact instead, and a guard asserts the scan still sees field
documentation so the new coverage cannot go vacuous.

Three descriptions claimed things the server has no way to observe or
enforce:

- open_app and resolve_app said a domain "derived from an app's name" is
  not resolvable. The server cannot know how a caller arrived at a URL;
  what it actually does is refuse an unknown bare name, and refuse an
  assumed origin that shows no Internet-Computer evidence. They now say
  that.
- discover_app_canisters promised every canister id found, while results
  are bounded at 50 (20 for unlabelled bundle literals) with the dropped
  count reported in `omitted`. The description now states the bound and
  the reporting.

The OpenAI submission doc's descriptions-match row loses a garbled
possessive and the claim that canister_update_call's description names
its financial refusal, which is no longer the design.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
One conflict, in SERVER_INSTRUCTIONS: this branch's factual instruction
text is kept, and its doc comment takes #163's correction — the policy is
stated server-wide and in no tool description, held there by a test that
now also covers the schemas.

The instruction text itself loses the same hedge sea-snake asked to
remove from the docs: it said "a curated list of known financial-service
canisters", which invites the reading that some services are outside the
list. It now says the financial-service canisters the guard carries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvwAVXfx5kLNc4SqKXkavk
…ster-disallow' into claude/icp-marketplace-compliance-vpawel-10-simplify-metadata
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants