@ggui-ai/protocol-conformance 0.6.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -18
- package/dist/binding-conformance/cases/bind-blueprint-no-contract-none.json +6 -0
- package/dist/binding-conformance/cases/bind-contract-without-tool-names-none.json +18 -0
- package/dist/binding-conformance/cases/bind-declared-gadget.json +13 -0
- package/dist/binding-conformance/cases/bind-declared-wins-over-contract.json +23 -0
- package/dist/binding-conformance/cases/bind-derived-caps-at-sixteen.json +54 -0
- package/dist/binding-conformance/cases/bind-derived-props-and-stream.json +36 -0
- package/dist/binding-conformance/cases/bind-derived-skips-invalid-names.json +40 -0
- package/dist/binding-conformance/cases/bind-derived-union-dedupes.json +38 -0
- package/dist/binding-conformance/cases/bind-gadget-undeclared-none.json +6 -0
- package/dist/binding-conformance/cases/bind-reject-charset.json +9 -0
- package/dist/binding-conformance/cases/bind-reject-duplicate-pair.json +12 -0
- package/dist/binding-conformance/cases/bind-reject-empty-list.json +9 -0
- package/dist/binding-conformance/cases/bind-reject-overlong-name.json +13 -0
- package/dist/binding-conformance/cases/bind-reject-server-charset.json +9 -0
- package/dist/binding-conformance/cases/bind-reject-too-many-entries.json +27 -0
- package/dist/binding-conformance/cases/filter-case-sensitive-server.json +7 -0
- package/dist/binding-conformance/cases/filter-case-sensitive-tool.json +7 -0
- package/dist/binding-conformance/cases/filter-none-passes-bound.json +7 -0
- package/dist/binding-conformance/cases/filter-none-passes-unbound.json +6 -0
- package/dist/binding-conformance/cases/filter-pair-bare-never-matches.json +7 -0
- package/dist/binding-conformance/cases/filter-pair-exact.json +10 -0
- package/dist/binding-conformance/cases/filter-pair-not-cross-entry.json +10 -0
- package/dist/binding-conformance/cases/filter-server-excludes-bare.json +7 -0
- package/dist/binding-conformance/cases/filter-server-match.json +7 -0
- package/dist/binding-conformance/cases/filter-tool-cross-server.json +7 -0
- package/dist/binding-conformance/cases/filter-tool-excludes-unbound.json +6 -0
- package/dist/binding-conformance/cases/filter-tool-matches-bare-entry.json +7 -0
- package/dist/binding-conformance/cases/filter-tool-no-match.json +7 -0
- package/dist/binding-conformance/index.d.ts +214 -0
- package/dist/binding-conformance/index.d.ts.map +1 -0
- package/dist/binding-conformance/index.js +153 -0
- package/dist/fixtures/consume-buffer/index.d.ts.map +1 -1
- package/dist/fixtures/consume-buffer/index.js +4 -1
- package/dist/fixtures/index.d.ts +42 -4
- package/dist/fixtures/index.d.ts.map +1 -1
- package/dist/fixtures/index.js +42 -4
- package/dist/fixtures/reserved-channel-authority/index.d.ts.map +1 -1
- package/dist/fixtures/reserved-channel-authority/index.js +4 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/match-behavior.d.ts.map +1 -1
- package/dist/match-behavior.js +4 -2
- package/dist/resolution-conformance/cases/resolve-bundle-host-computed.json +3 -3
- package/dist/resolution-conformance/cases/resolve-default-host.json +2 -2
- package/dist/resolution-conformance/cases/resolve-loopback-host-http-scheme.json +2 -2
- package/dist/resource-read-conformance/cases/all-ephemeral-substrate-fuses-like-none.json +29 -0
- package/dist/resource-read-conformance/cases/channel-less-read-mounts-inline.json +24 -0
- package/dist/resource-read-conformance/cases/evicted-row-remints-from-the-durable-record.json +26 -0
- package/dist/resource-read-conformance/cases/half-wired-blueprints-only-substrate-fuses-like-none.json +29 -0
- package/dist/resource-read-conformance/cases/half-wired-identity-only-substrate-fuses-like-none.json +29 -0
- package/dist/resource-read-conformance/cases/live-row-read-returns-mount-material.json +18 -0
- package/dist/resource-read-conformance/cases/malformed-locator-stays-outside-the-typed-set.json +19 -0
- package/dist/resource-read-conformance/cases/read-miss-answers-not-found.json +23 -0
- package/dist/resource-read-conformance/cases/read-on-a-substrate-less-server-answers-not-supported.json +22 -0
- package/dist/resource-read-conformance/cases/read-record-with-purged-blueprint-answers-blueprint-unresolvable.json +29 -0
- package/dist/resource-read-conformance/cases/read-refusal-is-indistinguishable-from-a-miss.json +47 -0
- package/dist/resource-read-conformance/cases/read-without-a-delivery-channel-answers-not-mountable.json +33 -0
- package/dist/resource-read-conformance/cases/registry-match-fuses-on-not-mountable.json +40 -0
- package/dist/resource-read-conformance/cases/substrate-less-fusion-of-refusal-and-miss.json +29 -0
- package/dist/resource-read-conformance/index.d.ts +383 -0
- package/dist/resource-read-conformance/index.d.ts.map +1 -0
- package/dist/resource-read-conformance/index.js +793 -0
- package/package.json +13 -3
package/README.md
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
# @ggui-ai/protocol-conformance
|
|
2
2
|
|
|
3
|
-
Conformance test kit for the ggui protocol. Ships
|
|
3
|
+
Conformance test kit for the ggui protocol. Ships JSON case catalogs + runners that drive them against a live implementation — the behavioral fixtures over WebSocket, the `resources/read` cases over the MCP binding — and report scorecard-style pass / fail / skip output.
|
|
4
4
|
|
|
5
5
|
This kit is the protocol's conformance surface: it lets a third-party MCP builder prove their implementation is protocol-conformant without reimplementing the test harness.
|
|
6
6
|
|
|
7
7
|
## Grading mechanisms
|
|
8
8
|
|
|
9
|
-
The kit grades protocol obligations through **
|
|
9
|
+
The kit grades protocol obligations through **four mechanisms**, each matched to where the obligation is observable:
|
|
10
10
|
|
|
11
11
|
1. **Path-A WS fixtures** — wire-observable behaviors graded from frames the runner collects over the live channel (acks, error frames, canonical `data` deliveries).
|
|
12
12
|
2. **Pure-function catalogs** — deterministic validation obligations (the SPEC §7.7.2 gadget obligations) graded against a caller-supplied function; no host, render, or transport.
|
|
13
13
|
3. **Session-state host read-back** — stateful obligations with no wire response (e.g. `host_context_observed` persisting onto `GguiSession.hostContext`): the runner dispatches the C→S frame, waits out the observation window, then reads the GguiSession field back through `ConformanceHost.readSessionField()` and deep-equals it against the fixture's `expected`. No host (or a host without the method, or a read that throws) → the fixture SKIPS with the reason — a host that cannot read state cannot grade it, and the kit never converts that gap into a pass.
|
|
14
|
+
4. **MCP-binding scenario driver** — request/response obligations on the MCP surface, where the deployment shape is part of what is under test. The `resources/read` catalog is the first: a case declares a server shape, a caller identity and a seed set, the adopter's driver brings a server up that way, and the kit reads locators against it and grades the raw JSON-RPC frames. A driver that cannot express a scenario throws, and the case SKIPS — never passes.
|
|
14
15
|
|
|
15
|
-
A
|
|
16
|
+
A fifth surface — the **Path-B browser-host driver** for DOM-level claims (`bootstrap-failure`, `props-update`) — is honestly absent: it is not yet packaged, and those fixtures skip wherever the kit runs (see below).
|
|
16
17
|
|
|
17
18
|
## Transport
|
|
18
19
|
|
|
19
|
-
**
|
|
20
|
+
**The behavioral fixture catalog is WebSocket-only.** The canonical ggui live-channel transport is WS (see SPEC §12 Transport Bindings). `TransportConfig` is shaped as an extensibly-closed union so later live-channel transports (HTTP long-poll) can be added without breaking the public API.
|
|
21
|
+
|
|
22
|
+
The MCP surface is graded separately, by catalog rather than by transport config — see `resource-read-conformance` below. Its cases are deliberately NOT registered in `fixturesByContract`: everything in that map is driven over a WebSocket, so an MCP case there would be a permanent skip on every WS run, which is the false gate the kit's exact skip-set pinning exists to prevent. `resources/read` is the only MCP method bound today; `tools/call` has no driver yet.
|
|
20
23
|
|
|
21
24
|
## Path-A vs Path-B matchability
|
|
22
25
|
|
|
@@ -42,17 +45,18 @@ The Path-B driver is not yet packaged — no browser-host harness ships with the
|
|
|
42
45
|
|
|
43
46
|
The partition is intentional: Path-A FAILs are vendor-neutrality bugs the server owns; Path-B SKIPs are not fails — they are claims a different driver is responsible for.
|
|
44
47
|
|
|
45
|
-
One declared grading gap on the action loop: `action-ack` proves the append half of the consume-buffer contract; the retrieval half (the agent draining the buffer via `ggui_consume`) is an MCP tool call a WS
|
|
48
|
+
One declared grading gap on the action loop: `action-ack` proves the append half of the consume-buffer contract; the retrieval half (the agent draining the buffer via `ggui_consume`) is an MCP **tool call**, which a WS runner cannot drive. The resource-read catalog below does not close it — that driver binds `resources/read`, and `tools/call` is a different method with a different seam. Grading it needs a tool-call driver, not a weaker WS assertion.
|
|
46
49
|
|
|
47
50
|
## Pure-function conformance catalogs
|
|
48
51
|
|
|
49
52
|
The behavioral fixture catalog above asserts what an implementation _does_ over the wire. Some protocol obligations aren't transport-observable at all — they are deterministic _validation functions_: given an input, accept or reject it. SPEC §7.7.2's gadget obligations are exactly this. Modeling them as WebSocket fixtures would mean faking wire frames the protocol never emits; instead the kit ships **pure-function catalogs** — accept/reject cases graded against a caller-supplied function, with no host, render, or transport.
|
|
50
53
|
|
|
51
|
-
Each catalog ships its cases as raw JSON so a non-TypeScript implementer can grade their own implementation, and each runner takes the implementation as a callback — the kit never hard-binds a concrete one.
|
|
54
|
+
Each catalog ships its cases as raw JSON so a non-TypeScript implementer can grade their own implementation, and each runner takes the implementation as a callback — the kit never hard-binds a concrete one. Four catalogs ship today:
|
|
52
55
|
|
|
53
56
|
- **`@ggui-ai/protocol-conformance/schema-conformance`** — which `DataContract.clientCapabilities` payloads a conformant parser MUST accept / reject (the gadget wire shape).
|
|
54
57
|
- **`@ggui-ai/protocol-conformance/registration-conformance`** — which `(contract, appGadgets, appPublicEnv)` triples the push-time gadget gate stack MUST accept / reject, and with which precise SPEC §7.9 reject code (`gadget_not_registered` / `gadget_package_mismatch` / `gadget_public_env_missing` / `duplicate_gadget_hook`).
|
|
55
58
|
- **`@ggui-ai/protocol-conformance/resolution-conformance`** — which bundle + style URLs the server MUST compute for a gadget descriptor's transport fields (`bundleHost` precedence, default host, loopback `http` scheme).
|
|
59
|
+
- **`@ggui-ai/protocol-conformance/binding-conformance`** — which MCP tool-binding set a manifest resolves to (a declared `mcpTools` list wins entirely; blueprints without it derive bare tool-name entries from contract tool names) and which artifacts the registry `tool=` / `server=` search filters MUST match (SPEC §7.7.4.1).
|
|
56
60
|
|
|
57
61
|
```ts
|
|
58
62
|
import { runSchemaConformance } from "@ggui-ai/protocol-conformance/schema-conformance";
|
|
@@ -72,29 +76,66 @@ if (schema.failed.length + gate.failed.length + urls.failed.length > 0) {
|
|
|
72
76
|
}
|
|
73
77
|
```
|
|
74
78
|
|
|
75
|
-
The `schema-conformance` meta-test binds its catalog to the live `@ggui-ai/protocol` `clientCapabilitiesSpecSchema` — a drift-catch if the wire schema diverges from the §7.7.2 obligations the catalog freezes. The `registration-conformance` and `
|
|
79
|
+
The `schema-conformance` meta-test binds its catalog to the live `@ggui-ai/protocol` `clientCapabilitiesSpecSchema` — a drift-catch if the wire schema diverges from the §7.7.2 obligations the catalog freezes. The `registration-conformance`, `resolution-conformance`, and `binding-conformance` meta-tests verify catalog coherence against faithful in-test implementations (the kit stays vendor-neutral — it does not depend on a server implementation); grading the _shipping_ gate / resolver is an implementation-side test that drives the corresponding runner.
|
|
80
|
+
|
|
81
|
+
## Resource-read conformance — the MCP binding
|
|
82
|
+
|
|
83
|
+
A read of a render locator (`ui://ggui/render/{sessionId}` or `ui://ggui/render/{sessionId}/{blueprintKey}`) has exactly **two exits**: a result whose contents declare a delivery channel — a live mount — or one typed JSON-RPC error. There is no third outcome, and in particular no successful result carrying a shell that can never paint anything.
|
|
84
|
+
|
|
85
|
+
That is what makes the read host-checkable. A host that gets `contents` back can mount them without inspecting anything; a host that gets an error routes on `error.data.code` — "come back never" versus "come back with a fresh render" — without parsing prose.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { runResourceReadConformance } from "@ggui-ai/protocol-conformance/resource-read-conformance";
|
|
89
|
+
|
|
90
|
+
const result = await runResourceReadConformance(async (scenario) => {
|
|
91
|
+
// Bring YOUR server up in `scenario.server`'s shape, as
|
|
92
|
+
// `scenario.caller`, with `scenario.seeds` applied.
|
|
93
|
+
const server = await bootMyServer(scenario);
|
|
94
|
+
return {
|
|
95
|
+
// Keys your registry assigned to `registered-blueprint` seeds.
|
|
96
|
+
registeredKeys: server.registeredKeys,
|
|
97
|
+
// MUST resolve. A protocol failure is `{kind: 'error'}` carrying
|
|
98
|
+
// the RAW frame — an MCP client's reconstructed error rewrites
|
|
99
|
+
// `message`, and the disclosure obligation is graded on bytes.
|
|
100
|
+
read: (uri) => server.readResource(uri),
|
|
101
|
+
dispose: () => server.close(),
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (result.failed.length > 0 || result.passed.length === 0) process.exit(1);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Throwing from the driver means "I cannot express this scenario" and the case **skips** with that reason — never passes. Throwing from `read` is a **fail**, because the read is the thing under test.
|
|
109
|
+
|
|
110
|
+
The catalog grades all four failure classes (`NOT_FOUND` on `-32002`; `BLUEPRINT_UNRESOLVABLE` / `NOT_SUPPORTED` / `NOT_MOUNTABLE` on `-32006`), the mount half on both a live row and a re-minted one, and the disclosure obligation: a read refused for lack of entitlement and a read of a locator that never existed must be **byte-identical**, on a server that keeps durable records, on one that keeps none, on the two half-wired shapes in between, and on one whose blueprint registry matches the probed key.
|
|
111
|
+
|
|
112
|
+
It deliberately does **not** grade: the order in which a substrate-less server answers (`NOT_SUPPORTED` describes the deployment, so answering it immediately is correct); `detail` wording on any code; the `NOT_FOUND` message literal (its _constancy_ is what is normative); internal-error message text; the number a URI naming no locator receives (that belongs to the transport binding — only the negative is graded, that it must not be one of the four); and the shell's markup (success is graded on the projected render meta, never on DOM shape).
|
|
76
113
|
|
|
77
114
|
## Conformance status
|
|
78
115
|
|
|
79
116
|
What is actually graded against shipping code today, and what is still awaiting a driver. This is an inventory, not a roadmap — a mechanism is listed as proven only if a test in the repo drives it against the real implementation on every push.
|
|
80
117
|
|
|
81
|
-
Every test below is a plain `*.test.ts` inside a package's `src/`, so it runs in that package's ordinary vitest suite. The ggui repo's per-push CI runs `turbo run test` across all non-e2e packages, so **all
|
|
118
|
+
Every test below is a plain `*.test.ts` inside a package's `src/`, so it runs in that package's ordinary vitest suite. The ggui repo's per-push CI runs `turbo run test` across all non-e2e packages, so **all eight rows below are gated on every push** — the kit needs no dedicated workflow, and adopters wiring it into their own CI need nothing more than their existing unit-test task.
|
|
82
119
|
|
|
83
|
-
| Obligation
|
|
84
|
-
|
|
|
85
|
-
| Gadget wire schema (§7.7.2)
|
|
86
|
-
| Registration gate (§7.7.3, §7.9)
|
|
87
|
-
| Bundle-URL resolution (§7.7.2)
|
|
88
|
-
| Path-A WS fixtures — first-party
|
|
89
|
-
| Path-A WS fixtures — vendor-neutral
|
|
120
|
+
| Obligation | Graded against | Driver |
|
|
121
|
+
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
|
|
122
|
+
| Gadget wire schema (§7.7.2) | the shipping `@ggui-ai/protocol` `clientCapabilitiesSpecSchema` | this kit's own `src/schema-conformance/schema-conformance.test.ts` |
|
|
123
|
+
| Registration gate (§7.7.3, §7.9) | the shipping gate trio in `@ggui-ai/mcp-server-handlers` | `mcp-server-handlers/src/renders/assert-gadgets.conformance.test.ts` |
|
|
124
|
+
| Bundle-URL resolution (§7.7.2) | the shipping `resolveGadgetUrls` in `@ggui-ai/mcp-server-handlers` | `mcp-server-handlers/src/renders/resolve-gadget-urls.conformance.test.ts` |
|
|
125
|
+
| Path-A WS fixtures — first-party | the real `@ggui-ai/mcp-server` GguiSession channel, booted in-process on an ephemeral port | `mcp-server/src/ggui-session-channel.conformance.test.ts` — **8 pass / 4 skip / 0 fail** |
|
|
126
|
+
| Path-A WS fixtures — vendor-neutral | `@ggui-ai/protocol-reference-server` | `protocol-reference-server/src/conformance.test.ts` — **9 pass / 3 skip / 0 fail** |
|
|
127
|
+
| `resources/read` typed failures + mount invariant | the shipping `registerGguiRenderResourceTemplate` in `@ggui-ai/mcp-server` | `mcp-server/src/mcp-apps-outbound.resource-read.conformance.test.ts` — **12 pass / 0 skip / 0 fail** |
|
|
128
|
+
| Tool-binding resolution — declared wins, contract derivation (§7.7.4.1) | the shipping `resolveMcpToolBindings` in `@ggui-ai/artifact-manifest` | `artifact-manifest/src/mcp-tool-bindings.conformance.test.ts` |
|
|
129
|
+
| Tool-binding search filters — `tool=` / `server=` semantics (§7.7.4.1) | the shipping `matchesMcpToolFilters` in `@ggui-ai/registry-core` | `registry-core/src/mcp-tool-filters.conformance.test.ts` |
|
|
90
130
|
|
|
91
|
-
Both WS drivers pin their pass set _and_ their skip set as exact sets. A fixture silently degrading to a skip fails the build, and so does a skipped fixture that quietly starts passing — a skip set that can grow unnoticed is a false gate, and re-pinning it is meant to be a deliberate act.
|
|
131
|
+
Both WS drivers pin their pass set _and_ their skip set as exact sets, and the resource-read driver pins its skip set as exactly empty. A fixture silently degrading to a skip fails the build, and so does a skipped fixture that quietly starts passing — a skip set that can grow unnoticed is a false gate, and re-pinning it is meant to be a deliberate act.
|
|
92
132
|
|
|
93
133
|
**Awaiting a driver:**
|
|
94
134
|
|
|
95
|
-
- **Path-B browser-host fixtures** — `bootstrap-bundle-fetch-failed`, `bootstrap-meta-missing` (both need MCP-Apps-host fault injection) and `props-update-roundtrip` (assertion is on rendered DOM). No browser-host adapter ships with the kit, so these skip on every driver above. Honestly absent, not silently passing.
|
|
135
|
+
- **Path-B browser-host fixtures** — `bootstrap-bundle-fetch-failed`, `bootstrap-meta-missing` (both need MCP-Apps-host fault injection) and `props-update-roundtrip` (assertion is on rendered DOM). No browser-host adapter ships with the kit, so these skip on every WS driver above. Honestly absent, not silently passing.
|
|
96
136
|
- **`version-mismatch` on the first-party server** — needs a `server-version-override` seam the channel deliberately does not expose; adding a production seam with no production caller purely to drive a fixture was rejected. It passes on the reference server, which has a per-render override.
|
|
97
|
-
- **The `ggui_consume` retrieval half of the action loop
|
|
137
|
+
- **The `ggui_consume` retrieval half of the action loop, and `stream-delivery-roundtrip`** — both are `tools/call` obligations (`ggui_consume`, `ggui_emit`). The resource-read driver binds `resources/read` only, so neither is closed by it; both await a tool-call driver.
|
|
138
|
+
- **`resources/read` on a second implementation** — the catalog is vendor-neutral by construction (the kit imports no server; the adopter supplies the driver), but it is graded against one implementation today. A second binding is what would ground the vendor-neutrality claim empirically, the way `protocol-reference-server` does for the WS catalog.
|
|
98
139
|
|
|
99
140
|
## Public API
|
|
100
141
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bind-blueprint-no-contract-none",
|
|
3
|
+
"description": "A blueprint manifest with no `mcpTools` and no contract. Nothing declares and nothing derives — the artifact has no bindings and is simply not findable by tool. Accept with no binding set.",
|
|
4
|
+
"entry": { "kind": "blueprint" },
|
|
5
|
+
"expect": { "outcome": "accept" }
|
|
6
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bind-contract-without-tool-names-none",
|
|
3
|
+
"description": "A blueprint manifest whose contract carries NO tool lineage anywhere derivation reads — props without `sourceTool`, stream channels without `source`. Derivation runs and finds the empty union: no bindings, same as having no contract at all.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"kind": "blueprint",
|
|
6
|
+
"contract": {
|
|
7
|
+
"propsSpec": {
|
|
8
|
+
"properties": {
|
|
9
|
+
"note": { "schema": { "description": "free-text note" } }
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"streamSpec": {
|
|
13
|
+
"ticks": { "schema": { "description": "local timer ticks" } }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"expect": { "outcome": "accept" }
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bind-declared-gadget",
|
|
3
|
+
"description": "A gadget manifest declaring `mcpTools` explicitly. The resolver MUST surface the declared list verbatim, marked `declared`. The list legally mixes a server-qualified entry with a bare (server-agnostic) entry for the same tool name — only exact-duplicate `(server, tool)` pairs are invalid, and a bare entry is a distinct pair from any server-qualified one.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"kind": "gadget",
|
|
6
|
+
"mcpTools": [{ "server": "acme-crm", "tool": "list_invoices" }, { "tool": "list_invoices" }]
|
|
7
|
+
},
|
|
8
|
+
"expect": {
|
|
9
|
+
"outcome": "accept",
|
|
10
|
+
"source": "declared",
|
|
11
|
+
"bindings": [{ "server": "acme-crm", "tool": "list_invoices" }, { "tool": "list_invoices" }]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bind-declared-wins-over-contract",
|
|
3
|
+
"description": "A blueprint manifest with BOTH a declared `mcpTools` list AND a contract whose prop lineage names a different tool. Declared wins entirely: derivation MUST NOT run and MUST NOT merge — the result carries only the declared entries, marked `declared`.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"kind": "blueprint",
|
|
6
|
+
"mcpTools": [{ "server": "acme-crm", "tool": "list_invoices" }],
|
|
7
|
+
"contract": {
|
|
8
|
+
"propsSpec": {
|
|
9
|
+
"properties": {
|
|
10
|
+
"forecast": {
|
|
11
|
+
"schema": { "description": "hourly forecast rows" },
|
|
12
|
+
"sourceTool": "get_forecast"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"expect": {
|
|
19
|
+
"outcome": "accept",
|
|
20
|
+
"source": "declared",
|
|
21
|
+
"bindings": [{ "server": "acme-crm", "tool": "list_invoices" }]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bind-derived-caps-at-sixteen",
|
|
3
|
+
"description": "Derivation caps at the same 16-entry bound a declared `mcpTools` list is schema-capped at, keeping the survivors in first-appearance order (props before stream, each in declaration order) and dropping anything past the 16th distinct tool name. This composes the existing envelope — it is not new policy.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"kind": "blueprint",
|
|
6
|
+
"contract": {
|
|
7
|
+
"propsSpec": {
|
|
8
|
+
"properties": {
|
|
9
|
+
"p01": { "schema": { "description": "p01" }, "sourceTool": "tool_01" },
|
|
10
|
+
"p02": { "schema": { "description": "p02" }, "sourceTool": "tool_02" },
|
|
11
|
+
"p03": { "schema": { "description": "p03" }, "sourceTool": "tool_03" },
|
|
12
|
+
"p04": { "schema": { "description": "p04" }, "sourceTool": "tool_04" },
|
|
13
|
+
"p05": { "schema": { "description": "p05" }, "sourceTool": "tool_05" },
|
|
14
|
+
"p06": { "schema": { "description": "p06" }, "sourceTool": "tool_06" },
|
|
15
|
+
"p07": { "schema": { "description": "p07" }, "sourceTool": "tool_07" },
|
|
16
|
+
"p08": { "schema": { "description": "p08" }, "sourceTool": "tool_08" },
|
|
17
|
+
"p09": { "schema": { "description": "p09" }, "sourceTool": "tool_09" },
|
|
18
|
+
"p10": { "schema": { "description": "p10" }, "sourceTool": "tool_10" }
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"streamSpec": {
|
|
22
|
+
"s11": { "schema": { "description": "s11" }, "source": { "tool": "tool_11" } },
|
|
23
|
+
"s12": { "schema": { "description": "s12" }, "source": { "tool": "tool_12" } },
|
|
24
|
+
"s13": { "schema": { "description": "s13" }, "source": { "tool": "tool_13" } },
|
|
25
|
+
"s14": { "schema": { "description": "s14" }, "source": { "tool": "tool_14" } },
|
|
26
|
+
"s15": { "schema": { "description": "s15" }, "source": { "tool": "tool_15" } },
|
|
27
|
+
"s16": { "schema": { "description": "s16" }, "source": { "tool": "tool_16" } },
|
|
28
|
+
"s17": { "schema": { "description": "s17" }, "source": { "tool": "tool_17" } }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"expect": {
|
|
33
|
+
"outcome": "accept",
|
|
34
|
+
"source": "derived",
|
|
35
|
+
"bindings": [
|
|
36
|
+
{ "tool": "tool_01" },
|
|
37
|
+
{ "tool": "tool_02" },
|
|
38
|
+
{ "tool": "tool_03" },
|
|
39
|
+
{ "tool": "tool_04" },
|
|
40
|
+
{ "tool": "tool_05" },
|
|
41
|
+
{ "tool": "tool_06" },
|
|
42
|
+
{ "tool": "tool_07" },
|
|
43
|
+
{ "tool": "tool_08" },
|
|
44
|
+
{ "tool": "tool_09" },
|
|
45
|
+
{ "tool": "tool_10" },
|
|
46
|
+
{ "tool": "tool_11" },
|
|
47
|
+
{ "tool": "tool_12" },
|
|
48
|
+
{ "tool": "tool_13" },
|
|
49
|
+
{ "tool": "tool_14" },
|
|
50
|
+
{ "tool": "tool_15" },
|
|
51
|
+
{ "tool": "tool_16" }
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bind-derived-props-and-stream",
|
|
3
|
+
"description": "A blueprint manifest with no `mcpTools` and a contract carrying tool lineage in both places derivation reads: `propsSpec.properties[*].sourceTool` and `streamSpec[*].source.tool`. The resolver MUST derive bare `{tool}` entries (contracts carry no server identity), marked `derived`, in first-appearance order: props entries in declaration order, then stream channels in declaration order.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"kind": "blueprint",
|
|
6
|
+
"contract": {
|
|
7
|
+
"propsSpec": {
|
|
8
|
+
"properties": {
|
|
9
|
+
"invoices": {
|
|
10
|
+
"schema": { "description": "invoice rows" },
|
|
11
|
+
"sourceTool": "list_invoices"
|
|
12
|
+
},
|
|
13
|
+
"customer": {
|
|
14
|
+
"schema": { "description": "customer record" },
|
|
15
|
+
"sourceTool": "get_customer"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"streamSpec": {
|
|
20
|
+
"invoiceEvents": {
|
|
21
|
+
"schema": { "description": "invoice change events" },
|
|
22
|
+
"source": { "tool": "watch_invoices" }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"expect": {
|
|
28
|
+
"outcome": "accept",
|
|
29
|
+
"source": "derived",
|
|
30
|
+
"bindings": [
|
|
31
|
+
{ "tool": "list_invoices" },
|
|
32
|
+
{ "tool": "get_customer" },
|
|
33
|
+
{ "tool": "watch_invoices" }
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bind-derived-skips-invalid-names",
|
|
3
|
+
"description": "Contract lineage tool names are agent/tool-authored data, not author declarations, so derivation stays inside the same charset envelope a declared `mcpTools` list is validated against by filtering invalid names silently rather than rejecting the manifest. An empty-string `sourceTool`, a charset-invalid `sourceTool` (embedded space), and a charset-invalid stream `source.tool` (embedded slash) MUST all be skipped; only names matching the binding charset survive derivation, in first-appearance order.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"kind": "blueprint",
|
|
6
|
+
"contract": {
|
|
7
|
+
"propsSpec": {
|
|
8
|
+
"properties": {
|
|
9
|
+
"empty": {
|
|
10
|
+
"schema": { "description": "prop with empty lineage" },
|
|
11
|
+
"sourceTool": ""
|
|
12
|
+
},
|
|
13
|
+
"invalidChars": {
|
|
14
|
+
"schema": { "description": "prop with charset-invalid lineage" },
|
|
15
|
+
"sourceTool": "has space"
|
|
16
|
+
},
|
|
17
|
+
"customer": {
|
|
18
|
+
"schema": { "description": "customer record" },
|
|
19
|
+
"sourceTool": "get_customer"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"streamSpec": {
|
|
24
|
+
"badChannel": {
|
|
25
|
+
"schema": { "description": "channel with charset-invalid lineage" },
|
|
26
|
+
"source": { "tool": "bad/tool" }
|
|
27
|
+
},
|
|
28
|
+
"invoiceEvents": {
|
|
29
|
+
"schema": { "description": "invoice change events" },
|
|
30
|
+
"source": { "tool": "watch_invoices" }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"expect": {
|
|
36
|
+
"outcome": "accept",
|
|
37
|
+
"source": "derived",
|
|
38
|
+
"bindings": [{ "tool": "get_customer" }, { "tool": "watch_invoices" }]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bind-derived-union-dedupes",
|
|
3
|
+
"description": "Derivation is a set union: the same tool name appearing on two props AND as a stream-channel source MUST collapse to one bare entry at its first appearance. Props without `sourceTool` and channels without `source` contribute nothing.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"kind": "blueprint",
|
|
6
|
+
"contract": {
|
|
7
|
+
"propsSpec": {
|
|
8
|
+
"properties": {
|
|
9
|
+
"openInvoices": {
|
|
10
|
+
"schema": { "description": "open invoice rows" },
|
|
11
|
+
"sourceTool": "list_invoices"
|
|
12
|
+
},
|
|
13
|
+
"paidInvoices": {
|
|
14
|
+
"schema": { "description": "paid invoice rows" },
|
|
15
|
+
"sourceTool": "list_invoices"
|
|
16
|
+
},
|
|
17
|
+
"title": {
|
|
18
|
+
"schema": { "description": "screen title" }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"streamSpec": {
|
|
23
|
+
"refresh": {
|
|
24
|
+
"schema": { "description": "refreshed invoice rows" },
|
|
25
|
+
"source": { "tool": "list_invoices" }
|
|
26
|
+
},
|
|
27
|
+
"ticker": {
|
|
28
|
+
"schema": { "description": "sourceless local ticks" }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"expect": {
|
|
34
|
+
"outcome": "accept",
|
|
35
|
+
"source": "derived",
|
|
36
|
+
"bindings": [{ "tool": "list_invoices" }]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bind-gadget-undeclared-none",
|
|
3
|
+
"description": "A gadget manifest without `mcpTools`. Gadgets have no contract to derive from — derivation is a blueprint-only fallback — so an undeclared gadget has no bindings.",
|
|
4
|
+
"entry": { "kind": "gadget" },
|
|
5
|
+
"expect": { "outcome": "accept" }
|
|
6
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bind-reject-charset",
|
|
3
|
+
"description": "A binding whose `tool` carries characters outside the allowed charset `[A-Za-z0-9_.-]` (here: a space and `!`). The manifest MUST be rejected with the manifest-validation code.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"kind": "gadget",
|
|
6
|
+
"mcpTools": [{ "tool": "list invoices!" }]
|
|
7
|
+
},
|
|
8
|
+
"expect": { "outcome": "reject", "code": "manifest_invalid" }
|
|
9
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bind-reject-duplicate-pair",
|
|
3
|
+
"description": "Two entries with the exact same `(server, tool)` pair. Exact duplicates are rejected at schema level with the manifest-validation code — deduplication is the publisher's job, not the registry's.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"kind": "blueprint",
|
|
6
|
+
"mcpTools": [
|
|
7
|
+
{ "server": "acme-crm", "tool": "list_invoices" },
|
|
8
|
+
{ "server": "acme-crm", "tool": "list_invoices" }
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"expect": { "outcome": "reject", "code": "manifest_invalid" }
|
|
12
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bind-reject-empty-list",
|
|
3
|
+
"description": "An empty `mcpTools` list. Present-but-empty is invalid — a publisher opting into tool bindings must name at least one; omitting the field entirely is the way to declare none.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"kind": "gadget",
|
|
6
|
+
"mcpTools": []
|
|
7
|
+
},
|
|
8
|
+
"expect": { "outcome": "reject", "code": "manifest_invalid" }
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bind-reject-overlong-name",
|
|
3
|
+
"description": "A binding whose `tool` is 129 characters — one past the 128-character bound. The manifest MUST be rejected with the manifest-validation code.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"kind": "gadget",
|
|
6
|
+
"mcpTools": [
|
|
7
|
+
{
|
|
8
|
+
"tool": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"expect": { "outcome": "reject", "code": "manifest_invalid" }
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bind-reject-server-charset",
|
|
3
|
+
"description": "A binding whose `server` violates the shared charset (here: a `/`). `server` and `tool` share one name rule. The manifest MUST be rejected with the manifest-validation code.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"kind": "blueprint",
|
|
6
|
+
"mcpTools": [{ "server": "acme/crm", "tool": "list_invoices" }]
|
|
7
|
+
},
|
|
8
|
+
"expect": { "outcome": "reject", "code": "manifest_invalid" }
|
|
9
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bind-reject-too-many-entries",
|
|
3
|
+
"description": "Seventeen binding entries — one past the 16-entry cap. The manifest MUST be rejected with the manifest-validation code.",
|
|
4
|
+
"entry": {
|
|
5
|
+
"kind": "blueprint",
|
|
6
|
+
"mcpTools": [
|
|
7
|
+
{ "tool": "tool_01" },
|
|
8
|
+
{ "tool": "tool_02" },
|
|
9
|
+
{ "tool": "tool_03" },
|
|
10
|
+
{ "tool": "tool_04" },
|
|
11
|
+
{ "tool": "tool_05" },
|
|
12
|
+
{ "tool": "tool_06" },
|
|
13
|
+
{ "tool": "tool_07" },
|
|
14
|
+
{ "tool": "tool_08" },
|
|
15
|
+
{ "tool": "tool_09" },
|
|
16
|
+
{ "tool": "tool_10" },
|
|
17
|
+
{ "tool": "tool_11" },
|
|
18
|
+
{ "tool": "tool_12" },
|
|
19
|
+
{ "tool": "tool_13" },
|
|
20
|
+
{ "tool": "tool_14" },
|
|
21
|
+
{ "tool": "tool_15" },
|
|
22
|
+
{ "tool": "tool_16" },
|
|
23
|
+
{ "tool": "tool_17" }
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"expect": { "outcome": "reject", "code": "manifest_invalid" }
|
|
27
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "filter-case-sensitive-server",
|
|
3
|
+
"description": "Server matching is case-sensitive exact — `acme-crm` does not match an entry declaring `Acme-CRM`, even with an exact tool match on the same entry.",
|
|
4
|
+
"bindings": [{ "server": "Acme-CRM", "tool": "list_invoices" }],
|
|
5
|
+
"filters": { "server": "acme-crm", "tool": "list_invoices" },
|
|
6
|
+
"expect": false
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "filter-case-sensitive-tool",
|
|
3
|
+
"description": "Tool matching is case-sensitive exact — MCP tool names are case-sensitive, so `list_invoices` does not match an entry bound to `List_Invoices`.",
|
|
4
|
+
"bindings": [{ "tool": "List_Invoices" }],
|
|
5
|
+
"filters": { "tool": "list_invoices" },
|
|
6
|
+
"expect": false
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "filter-none-passes-bound",
|
|
3
|
+
"description": "No tool/server filters at all: the predicate passes every artifact — the binding dimensions only constrain when asked, preserving AND-composition with unrelated search filters.",
|
|
4
|
+
"bindings": [{ "server": "acme-crm", "tool": "list_invoices" }],
|
|
5
|
+
"filters": {},
|
|
6
|
+
"expect": true
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "filter-pair-bare-never-matches",
|
|
3
|
+
"description": "A bare entry never satisfies a pair query even when the tool name matches — the server dimension requires an explicit server declaration.",
|
|
4
|
+
"bindings": [{ "tool": "list_invoices" }],
|
|
5
|
+
"filters": { "server": "acme-crm", "tool": "list_invoices" },
|
|
6
|
+
"expect": false
|
|
7
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "filter-pair-exact",
|
|
3
|
+
"description": "`tool=` + `server=` together match only an entry carrying exactly that `(server, tool)` pair.",
|
|
4
|
+
"bindings": [
|
|
5
|
+
{ "server": "acme-crm", "tool": "list_invoices" },
|
|
6
|
+
{ "server": "other-crm", "tool": "export_csv" }
|
|
7
|
+
],
|
|
8
|
+
"filters": { "server": "acme-crm", "tool": "list_invoices" },
|
|
9
|
+
"expect": true
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "filter-pair-not-cross-entry",
|
|
3
|
+
"description": "The pair filter is per-entry, not per-list: one entry matching the server and a DIFFERENT entry matching the tool is NOT a match — both dimensions must land on the same entry.",
|
|
4
|
+
"bindings": [
|
|
5
|
+
{ "server": "acme-crm", "tool": "create_invoice" },
|
|
6
|
+
{ "server": "other-crm", "tool": "list_invoices" }
|
|
7
|
+
],
|
|
8
|
+
"filters": { "server": "acme-crm", "tool": "list_invoices" },
|
|
9
|
+
"expect": false
|
|
10
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "filter-server-excludes-bare",
|
|
3
|
+
"description": "`server=` alone NEVER matches a bare entry — a server-agnostic binding declares no server, so it cannot satisfy a server query.",
|
|
4
|
+
"bindings": [{ "tool": "list_invoices" }],
|
|
5
|
+
"filters": { "server": "acme-crm" },
|
|
6
|
+
"expect": false
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "filter-server-match",
|
|
3
|
+
"description": "`server=` alone matches any entry that explicitly declares that server, whatever its tool.",
|
|
4
|
+
"bindings": [{ "server": "acme-crm", "tool": "list_invoices" }],
|
|
5
|
+
"filters": { "server": "acme-crm" },
|
|
6
|
+
"expect": true
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "filter-tool-cross-server",
|
|
3
|
+
"description": "`tool=` alone matches any entry carrying that tool name regardless of which server qualifies it — a server-qualified entry satisfies a bare tool query.",
|
|
4
|
+
"bindings": [{ "server": "acme-crm", "tool": "list_invoices" }, { "tool": "export_csv" }],
|
|
5
|
+
"filters": { "tool": "list_invoices" },
|
|
6
|
+
"expect": true
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "filter-tool-matches-bare-entry",
|
|
3
|
+
"description": "`tool=` alone also matches a bare (server-agnostic) entry — the tool dimension ignores server identity entirely.",
|
|
4
|
+
"bindings": [{ "tool": "list_invoices" }],
|
|
5
|
+
"filters": { "tool": "list_invoices" },
|
|
6
|
+
"expect": true
|
|
7
|
+
}
|