@ggui-ai/protocol-conformance 0.7.0 → 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 +15 -12
- 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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- 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/channel-less-read-mounts-inline.json +24 -0
- package/dist/resource-read-conformance/cases/read-without-a-delivery-channel-answers-not-mountable.json +15 -4
- package/dist/resource-read-conformance/index.d.ts +16 -4
- package/dist/resource-read-conformance/index.d.ts.map +1 -1
- package/dist/resource-read-conformance/index.js +16 -5
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -51,11 +51,12 @@ One declared grading gap on the action loop: `action-ack` proves the append half
|
|
|
51
51
|
|
|
52
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.
|
|
53
53
|
|
|
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.
|
|
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:
|
|
55
55
|
|
|
56
56
|
- **`@ggui-ai/protocol-conformance/schema-conformance`** — which `DataContract.clientCapabilities` payloads a conformant parser MUST accept / reject (the gadget wire shape).
|
|
57
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`).
|
|
58
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).
|
|
59
60
|
|
|
60
61
|
```ts
|
|
61
62
|
import { runSchemaConformance } from "@ggui-ai/protocol-conformance/schema-conformance";
|
|
@@ -75,7 +76,7 @@ if (schema.failed.length + gate.failed.length + urls.failed.length > 0) {
|
|
|
75
76
|
}
|
|
76
77
|
```
|
|
77
78
|
|
|
78
|
-
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.
|
|
79
80
|
|
|
80
81
|
## Resource-read conformance — the MCP binding
|
|
81
82
|
|
|
@@ -114,16 +115,18 @@ It deliberately does **not** grade: the order in which a substrate-less server a
|
|
|
114
115
|
|
|
115
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.
|
|
116
117
|
|
|
117
|
-
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
|
-
|
|
119
|
-
| Obligation
|
|
120
|
-
|
|
|
121
|
-
| Gadget wire schema (§7.7.2)
|
|
122
|
-
| Registration gate (§7.7.3, §7.9)
|
|
123
|
-
| Bundle-URL resolution (§7.7.2)
|
|
124
|
-
| Path-A WS fixtures — first-party
|
|
125
|
-
| Path-A WS fixtures — vendor-neutral
|
|
126
|
-
| `resources/read` typed failures + mount invariant
|
|
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.
|
|
119
|
+
|
|
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` |
|
|
127
130
|
|
|
128
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.
|
|
129
132
|
|
|
@@ -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
|
+
}
|