@openwop/openwop-conformance 1.133.0 → 1.134.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/package.json +1 -1
- package/schemas/CORPUS-STAMP.json +2 -2
- package/src/lib/driver.ts +3 -1
- package/src/lib/mcp-mount.ts +39 -0
- package/src/scenarios/mcp-2026-07-28-discover.test.ts +5 -3
- package/src/scenarios/mcp-cache-tenant-scope.test.ts +4 -2
- package/src/scenarios/mcp-current-auth-boundary.test.ts +5 -3
- package/src/scenarios/mcp-extension-opacity.test.ts +2 -1
- package/src/scenarios/mcp-mrtr-roundtrip.test.ts +4 -3
- package/src/scenarios/mcp-server-elicitation-bridge.test.ts +4 -2
- package/src/scenarios/mcp-server-prompt-roundtrip.test.ts +4 -2
- package/src/scenarios/mcp-server-resource-roundtrip.test.ts +4 -2
- package/src/scenarios/mcp-server-sampling-bridge.test.ts +4 -2
- package/src/scenarios/mcp-server-tool-roundtrip.test.ts +4 -2
- package/src/scenarios/mcp-server-untrusted-args.test.ts +5 -3
- package/src/scenarios/mcp-stateless-request.test.ts +6 -5
- package/src/scenarios/mcp-version-negotiation.test.ts +3 -3
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Provenance of this vendored schemas/ copy. See conformance/README.md \u00a7\"Resolving the contract\". Compare against the stamp in your installed @openwop/openwop-conformance to detect a stale hand-copied contract.",
|
|
3
|
-
"suiteVersion": "1.
|
|
4
|
-
"corpusCommit": "
|
|
3
|
+
"suiteVersion": "1.134.0",
|
|
4
|
+
"corpusCommit": "fbb82fa8ea94c25358a63fda98bd49fdb528693a"
|
|
5
5
|
}
|
package/src/lib/driver.ts
CHANGED
|
@@ -35,7 +35,9 @@ class OpenWOPDriver {
|
|
|
35
35
|
init: OpenWOPRequestInit = {},
|
|
36
36
|
): Promise<OpenWOPResponse> {
|
|
37
37
|
const env = loadEnv();
|
|
38
|
-
|
|
38
|
+
// An absolute URL is used as-is (a host may advertise its MCP server mount or
|
|
39
|
+
// an A2A endpoint on another origin); a path is joined to the base URL.
|
|
40
|
+
const url = /^https?:\/\//i.test(path) ? path : `${env.baseUrl}${path}`;
|
|
39
41
|
|
|
40
42
|
const headers: Record<string, string> = {
|
|
41
43
|
Accept: 'application/json',
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a host mounts its MCP server (RFC 0153 §B / `mcp-integration.md`
|
|
3
|
+
* §"MCP server composition").
|
|
4
|
+
*
|
|
5
|
+
* The reference boots mount it at the sample-seam path
|
|
6
|
+
* `/v1/host/sample/mcp`, and until suite 1.134.0 every server-side MCP
|
|
7
|
+
* scenario hard-coded that path. The v1.0 discovery baseline has always let a
|
|
8
|
+
* host SAY where its mount is — `capabilities.mcp.serverUrls: string[]`
|
|
9
|
+
* (`mcp-discoverability.test.ts`) — and the first deployed host to advertise
|
|
10
|
+
* `mcp-2026-07-28` mounted at `/v1/host/openwop-app/mcp`, advertised exactly
|
|
11
|
+
* that, and watched ten `server/discover` legs 404 against the sample path and
|
|
12
|
+
* return early (S25, openwop-app H38, 2026-08-17). So: read the advert first,
|
|
13
|
+
* fall back to the sample path, and never treat "the mount answered 404" as a
|
|
14
|
+
* pass — that is `seamAbsent` (the host advertised a server mount it does not
|
|
15
|
+
* serve where it said).
|
|
16
|
+
*/
|
|
17
|
+
import { driver } from './driver.js';
|
|
18
|
+
import { capabilityFamily } from './discovery-capabilities.js';
|
|
19
|
+
|
|
20
|
+
export const SAMPLE_MCP_MOUNT = '/v1/host/sample/mcp';
|
|
21
|
+
|
|
22
|
+
interface McpAdvert {
|
|
23
|
+
readonly supported?: boolean;
|
|
24
|
+
readonly serverUrls?: unknown;
|
|
25
|
+
readonly serverMount?: { readonly supported?: boolean };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The MCP server mount to POST JSON-RPC to: `capabilities.mcp.serverUrls[0]`
|
|
30
|
+
* when the host advertises one (a path is joined to the base URL by the driver;
|
|
31
|
+
* an absolute URL is used as-is), else the sample-seam path.
|
|
32
|
+
*/
|
|
33
|
+
export async function mcpServerMount(): Promise<string> {
|
|
34
|
+
const disco = await driver.get('/.well-known/openwop');
|
|
35
|
+
const mcp = capabilityFamily<McpAdvert>(disco.json, 'mcp');
|
|
36
|
+
const urls = Array.isArray(mcp?.serverUrls) ? (mcp?.serverUrls as unknown[]) : [];
|
|
37
|
+
const first = urls.find((u): u is string => typeof u === 'string' && u.length > 0);
|
|
38
|
+
return first ?? SAMPLE_MCP_MOUNT;
|
|
39
|
+
}
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
|
|
26
26
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
27
27
|
import { driver } from '../lib/driver.js';
|
|
28
|
+
import { seamAbsent } from '../lib/soft-skip.js';
|
|
29
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
28
30
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
29
31
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
30
32
|
import { McpFakeServer, MCP_ERR } from '../lib/mcp-fake-server.js';
|
|
@@ -170,7 +172,7 @@ async function claimsCurrent(): Promise<boolean> {
|
|
|
170
172
|
}
|
|
171
173
|
async function hostRpc(method: string, params: Record<string, unknown>, headers: Record<string, string>) {
|
|
172
174
|
// The reference host mounts its MCP server at /v1/host/sample/mcp (mcp-server-* scenarios).
|
|
173
|
-
const res = await driver.post(
|
|
175
|
+
const res = await driver.post(await mcpServerMount(), { jsonrpc: '2.0', id: 1, method, params }, { headers });
|
|
174
176
|
return { status: res.status, body: res.json as { result?: Record<string, unknown>; error?: { code: number; data?: Record<string, unknown> } } };
|
|
175
177
|
}
|
|
176
178
|
|
|
@@ -179,7 +181,7 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §B — host as MCP ser
|
|
|
179
181
|
if (!behaviorGate(PROFILE, await claimsCurrent())) return;
|
|
180
182
|
const caps = (await mcp())!;
|
|
181
183
|
const r = await hostRpc('server/discover', { _meta: { [META_V]: '2026-07-28', [META_C]: {} } }, { 'MCP-Protocol-Version': '2026-07-28', 'Mcp-Method': 'server/discover' });
|
|
182
|
-
if (r.status === 404 || r.status === 403) return
|
|
184
|
+
if (r.status === 404 || r.status === 403) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${r.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
183
185
|
expect(r.status, driver.describe('mcp-integration.md §B', 'server/discover is a server MUST under 2026-07-28')).toBe(200);
|
|
184
186
|
expect(r.body.result?.['resultType']).toBe('complete');
|
|
185
187
|
expect([...((r.body.result?.['supportedVersions'] as string[] | undefined) ?? [])].sort(), driver.describe('mcp-integration.md §B', 'server/discover.supportedVersions MUST equal capabilities.mcp.protocolVersions — two documents, one fact')).toEqual([...(caps.protocolVersions ?? [])].sort());
|
|
@@ -196,7 +198,7 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §B — host as MCP ser
|
|
|
196
198
|
// answered -32022 — the leg was testing an unstated precedence
|
|
197
199
|
// (mcp-integration.md §B: agreement is checked before selection).
|
|
198
200
|
const mismatch = await hostRpc('tools/list', { _meta: { [META_V]: '2025-06-18', [META_C]: {} } }, { 'MCP-Protocol-Version': '2026-07-28', 'Mcp-Method': 'tools/list' });
|
|
199
|
-
if (mismatch.status === 404 || mismatch.status === 403) return;
|
|
201
|
+
if (mismatch.status === 404 || mismatch.status === 403) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${mismatch.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
200
202
|
expect(mismatch.status, driver.describe('mcp-integration.md §B', 'header ≠ body MUST be refused 400 (HeaderMismatchError -32020) — fail closed')).toBe(400);
|
|
201
203
|
expect(mismatch.body.error?.code).toBe(MCP_ERR.HEADER_MISMATCH);
|
|
202
204
|
const unsupported = await hostRpc('tools/list', { _meta: { [META_V]: '1999-01-01', [META_C]: {} } }, { 'MCP-Protocol-Version': '1999-01-01', 'Mcp-Method': 'tools/list' });
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
|
|
23
23
|
import { describe, it, expect } from 'vitest';
|
|
24
24
|
import { driver } from '../lib/driver.js';
|
|
25
|
+
import { seamAbsent } from '../lib/soft-skip.js';
|
|
26
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
25
27
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
26
28
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
27
29
|
|
|
@@ -38,7 +40,7 @@ async function claimsCurrent(): Promise<boolean> {
|
|
|
38
40
|
async function listAs(bearer?: string) {
|
|
39
41
|
const headers: Record<string, string> = { 'MCP-Protocol-Version': '2026-07-28', 'Mcp-Method': 'tools/list' };
|
|
40
42
|
if (bearer) headers['authorization'] = `Bearer ${bearer}`;
|
|
41
|
-
const res = await driver.post(
|
|
43
|
+
const res = await driver.post(await mcpServerMount(), { jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: { [META_V]: '2026-07-28', [META_C]: {} } } }, { headers });
|
|
42
44
|
return { status: res.status, body: res.json as { result?: { tools?: unknown[]; cacheScope?: string; ttlMs?: number } } };
|
|
43
45
|
}
|
|
44
46
|
|
|
@@ -46,7 +48,7 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §D — mcp-cache-tenan
|
|
|
46
48
|
it('a per-caller list is cacheScope private; a public list is byte-identical across callers', async () => {
|
|
47
49
|
if (!behaviorGate(PROFILE, await claimsCurrent())) return;
|
|
48
50
|
const mine = await listAs();
|
|
49
|
-
if (mine.status === 404 || mine.status === 403) return;
|
|
51
|
+
if (mine.status === 404 || mine.status === 403) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${mine.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
50
52
|
expect(['public', 'private'], driver.describe('mcp-integration.md §D', 'cacheScope MUST be present on tools/list')).toContain(mine.body.result?.cacheScope);
|
|
51
53
|
const other = process.env.OPENWOP_TEST_SECONDARY_API_KEY;
|
|
52
54
|
if (!other) {
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
|
|
19
19
|
import { describe, it, expect } from 'vitest';
|
|
20
20
|
import { driver } from '../lib/driver.js';
|
|
21
|
+
import { seamAbsent } from '../lib/soft-skip.js';
|
|
22
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
21
23
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
22
24
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
23
25
|
|
|
@@ -38,10 +40,10 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §E — mcp-current-aut
|
|
|
38
40
|
const { mcp, anon } = await disco();
|
|
39
41
|
const claims = mcp?.supported === true && (mcp.profiles ?? []).includes(PROFILE) && mcp.serverMount?.supported === true;
|
|
40
42
|
if (!behaviorGate(PROFILE, claims)) return;
|
|
41
|
-
const authed = await driver.post(
|
|
42
|
-
if (authed.status === 404 || authed.status === 403) return
|
|
43
|
+
const authed = await driver.post(await mcpServerMount(), REQ, { headers: HDR });
|
|
44
|
+
if (authed.status === 404 || authed.status === 403) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${authed.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
43
45
|
expect(authed.status, driver.describe('mcp-integration.md §E', 'the authenticated call MUST succeed at the same path, so a refusal below is not a wrong path')).toBe(200);
|
|
44
|
-
const anonymous = await driver.post(
|
|
46
|
+
const anonymous = await driver.post(await mcpServerMount(), REQ, { headers: HDR, authenticated: false });
|
|
45
47
|
if (anon?.supported === true) {
|
|
46
48
|
// Anonymous is permitted only through the RFC 0132 surface; a 200 here is that surface answering.
|
|
47
49
|
expect([200, 401, 403]).toContain(anonymous.status);
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { describe, it, expect } from 'vitest';
|
|
24
24
|
import { softSkip, seamAbsent } from '../lib/soft-skip.js';
|
|
25
25
|
import { driver } from '../lib/driver.js';
|
|
26
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
26
27
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
27
28
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
28
29
|
import { getMcpFakeServer } from '../lib/mcp-fake-server.js';
|
|
@@ -66,7 +67,7 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §D — mcp-extension-o
|
|
|
66
67
|
const caps = await mcp();
|
|
67
68
|
const claims = caps?.supported === true && (caps.profiles ?? []).includes('mcp-2026-07-28') && caps.serverMount?.supported === true;
|
|
68
69
|
if (!behaviorGate('mcp-2026-07-28', claims)) return;
|
|
69
|
-
const res = await driver.post(
|
|
70
|
+
const res = await driver.post(await mcpServerMount(), {
|
|
70
71
|
jsonrpc: '2.0', id: 1, method: 'tools/list',
|
|
71
72
|
params: { _meta: { [META_V]: '2026-07-28', [META_C]: { extensions: { 'io.example/authority': { admin: true } } }, 'io.example/authority': { grantScopes: ['*'] } } },
|
|
72
73
|
}, { headers: { 'MCP-Protocol-Version': '2026-07-28', 'Mcp-Method': 'tools/list' } });
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
import { describe, it, expect } from 'vitest';
|
|
34
34
|
import { softSkip, seamAbsent } from '../lib/soft-skip.js';
|
|
35
35
|
import { driver } from '../lib/driver.js';
|
|
36
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
36
37
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
37
38
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
38
39
|
import { getMcpFakeServer } from '../lib/mcp-fake-server.js';
|
|
@@ -120,7 +121,7 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §C — mcp-mrtr-roundt
|
|
|
120
121
|
// not seam absence and not a host finding — fail loudly at the source.
|
|
121
122
|
expect(reg.status, driver.describe('host-sample-test-seams.md §"sample workflows"', `the registration fixture MUST be accepted; a 4xx means the fixture this leg posts is malformed (suite defect): ${JSON.stringify(reg.json).slice(0, 200)}`)).toBeLessThan(400);
|
|
122
123
|
const hdr = { 'MCP-Protocol-Version': '2026-07-28', 'Mcp-Method': 'tools/call', 'Mcp-Name': TOOL };
|
|
123
|
-
const first = await driver.post(
|
|
124
|
+
const first = await driver.post(await mcpServerMount(), { jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: TOOL, arguments: {}, _meta: { [META_V]: '2026-07-28', [META_C]: { elicitation: {} } } } }, { headers: hdr });
|
|
124
125
|
if (first.status === 404) return seamAbsent('host advertises an MCP server mount but /v1/host/sample/mcp answered 404');
|
|
125
126
|
const r1 = first.json as { result?: { resultType?: string; inputRequests?: Record<string, { method?: string }>; requestState?: string }; error?: { code: number } };
|
|
126
127
|
expect(r1.error, driver.describe('mcp-integration.md §C.2', `tools/call MUST NOT error: ${JSON.stringify(r1.error)}`)).toBeUndefined();
|
|
@@ -129,11 +130,11 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §C — mcp-mrtr-roundt
|
|
|
129
130
|
expect(key, driver.describe('mcp-integration.md §C.2', 'inputRequests MUST carry the elicitation')).toBeDefined();
|
|
130
131
|
expect(r1.result?.inputRequests?.[key!]?.method).toBe('elicitation/create');
|
|
131
132
|
expect(typeof r1.result?.requestState, driver.describe('mcp-integration.md §C.2', 'requestState MUST be present (opaque, integrity-protected, bound to principal/TTL/request/runId/interrupt token)')).toBe('string');
|
|
132
|
-
const retry = await driver.post(
|
|
133
|
+
const retry = await driver.post(await mcpServerMount(), { jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: TOOL, arguments: {}, requestState: r1.result!.requestState, inputResponses: { [key!]: { action: 'accept', content: { name: 'Ada' } } }, _meta: { [META_V]: '2026-07-28', [META_C]: { elicitation: {} } } } }, { headers: hdr });
|
|
133
134
|
const r2 = retry.json as { result?: { resultType?: string }; error?: { code: number } };
|
|
134
135
|
expect(r2.error).toBeUndefined();
|
|
135
136
|
expect(['complete', 'input_required'], driver.describe('mcp-integration.md §C.2', 'the retry MUST resolve the interrupt (complete) or ask for the next pending input')).toContain(r2.result?.resultType);
|
|
136
|
-
const forged = await driver.post(
|
|
137
|
+
const forged = await driver.post(await mcpServerMount(), { jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: TOOL, arguments: {}, requestState: 'forged', inputResponses: { [key!]: { action: 'accept', content: { name: 'Eve' } } }, _meta: { [META_V]: '2026-07-28', [META_C]: { elicitation: {} } } } }, { headers: hdr });
|
|
137
138
|
const r3 = forged.json as { result?: { resultType?: string }; error?: { code: number } };
|
|
138
139
|
expect(r3.error !== undefined || forged.status >= 400, driver.describe('mcp-integration.md §C.2', 'a requestState that fails integrity verification MUST be refused (upstream: attacker-controlled input)')).toBe(true);
|
|
139
140
|
});
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
import { describe, it, expect } from 'vitest';
|
|
15
15
|
import { driver } from '../lib/driver.js';
|
|
16
|
+
import { seamAbsent } from '../lib/soft-skip.js';
|
|
17
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
16
18
|
|
|
17
19
|
interface DiscoveryDoc {
|
|
18
20
|
capabilities?: Record<string, unknown>;
|
|
@@ -31,7 +33,7 @@ async function rpc(method: string, params?: Record<string, unknown>) {
|
|
|
31
33
|
const id = Math.floor(Math.random() * 1e6);
|
|
32
34
|
const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
|
|
33
35
|
if (params !== undefined) req.params = params;
|
|
34
|
-
const res = await driver.post(
|
|
36
|
+
const res = await driver.post(await mcpServerMount(), req);
|
|
35
37
|
return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string } } };
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -71,7 +73,7 @@ describe('mcp-server-elicitation-bridge: behavioral (RFC 0020 §A point 3)', ()
|
|
|
71
73
|
required: ['name'],
|
|
72
74
|
},
|
|
73
75
|
});
|
|
74
|
-
if (r.status === 404) return;
|
|
76
|
+
if (r.status === 404) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${r.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
75
77
|
expect(r.status, 'JSON-RPC envelope MUST 200').toBe(200);
|
|
76
78
|
const dispatched = !!r.body.result || (!!r.body.error && r.body.error.code !== -32601);
|
|
77
79
|
expect(
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import { describe, it, expect } from 'vitest';
|
|
10
10
|
import { driver } from '../lib/driver.js';
|
|
11
|
+
import { seamAbsent } from '../lib/soft-skip.js';
|
|
12
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
11
13
|
|
|
12
14
|
interface DiscoveryDoc {
|
|
13
15
|
capabilities?: Record<string, unknown>;
|
|
@@ -26,7 +28,7 @@ async function rpc(method: string, params?: Record<string, unknown>) {
|
|
|
26
28
|
const id = Math.floor(Math.random() * 1e6);
|
|
27
29
|
const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
|
|
28
30
|
if (params !== undefined) req.params = params;
|
|
29
|
-
const res = await driver.post(
|
|
31
|
+
const res = await driver.post(await mcpServerMount(), req);
|
|
30
32
|
return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string } } };
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -65,7 +67,7 @@ describe('mcp-server-prompt-roundtrip: behavioral (RFC 0020)', () => {
|
|
|
65
67
|
if (!(await registerPromptWorkflow())) return;
|
|
66
68
|
|
|
67
69
|
const list = await rpc('prompts/list');
|
|
68
|
-
if (list.status === 404) return;
|
|
70
|
+
if (list.status === 404) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${list.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
69
71
|
const prompts = (list.body.result as { prompts?: Array<{ name: string }> } | undefined)?.prompts ?? [];
|
|
70
72
|
expect(
|
|
71
73
|
prompts.find((p) => p.name === PROMPT_NAME),
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import { describe, it, expect } from 'vitest';
|
|
12
12
|
import { driver } from '../lib/driver.js';
|
|
13
|
+
import { seamAbsent } from '../lib/soft-skip.js';
|
|
14
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
13
15
|
|
|
14
16
|
interface DiscoveryDoc {
|
|
15
17
|
capabilities?: Record<string, unknown>;
|
|
@@ -28,7 +30,7 @@ async function rpc(method: string, params?: Record<string, unknown>) {
|
|
|
28
30
|
const id = Math.floor(Math.random() * 1e6);
|
|
29
31
|
const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
|
|
30
32
|
if (params !== undefined) req.params = params;
|
|
31
|
-
const res = await driver.post(
|
|
33
|
+
const res = await driver.post(await mcpServerMount(), req);
|
|
32
34
|
return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string } } };
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -67,7 +69,7 @@ describe('mcp-server-resource-roundtrip: behavioral (RFC 0020)', () => {
|
|
|
67
69
|
if (!(await registerResourceWorkflow())) return;
|
|
68
70
|
|
|
69
71
|
const list = await rpc('resources/list');
|
|
70
|
-
if (list.status === 404) return;
|
|
72
|
+
if (list.status === 404) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${list.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
71
73
|
const resources = (list.body.result as { resources?: Array<{ uri: string }> } | undefined)?.resources ?? [];
|
|
72
74
|
expect(
|
|
73
75
|
resources.find((r) => r.uri === RESOURCE_URI),
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
import { describe, it, expect } from 'vitest';
|
|
18
18
|
import { driver } from '../lib/driver.js';
|
|
19
|
+
import { seamAbsent } from '../lib/soft-skip.js';
|
|
20
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
19
21
|
|
|
20
22
|
interface DiscoveryDoc {
|
|
21
23
|
capabilities?: Record<string, unknown>;
|
|
@@ -34,7 +36,7 @@ async function rpc(method: string, params?: Record<string, unknown>) {
|
|
|
34
36
|
const id = Math.floor(Math.random() * 1e6);
|
|
35
37
|
const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
|
|
36
38
|
if (params !== undefined) req.params = params;
|
|
37
|
-
const res = await driver.post(
|
|
39
|
+
const res = await driver.post(await mcpServerMount(), req);
|
|
38
40
|
return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string } } };
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -70,7 +72,7 @@ describe('mcp-server-sampling-bridge: behavioral (RFC 0020 §A point 3)', () =>
|
|
|
70
72
|
messages: [{ role: 'user', content: { type: 'text', text: 'ping' } }],
|
|
71
73
|
maxTokens: 16,
|
|
72
74
|
});
|
|
73
|
-
if (r.status === 404) return;
|
|
75
|
+
if (r.status === 404) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${r.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
74
76
|
expect(r.status, 'JSON-RPC envelope MUST 200').toBe(200);
|
|
75
77
|
const dispatched = !!r.body.result || (!!r.body.error && r.body.error.code !== -32601);
|
|
76
78
|
expect(
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import { describe, it, expect } from 'vitest';
|
|
16
16
|
import { driver } from '../lib/driver.js';
|
|
17
|
+
import { seamAbsent } from '../lib/soft-skip.js';
|
|
18
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
17
19
|
|
|
18
20
|
interface DiscoveryDoc {
|
|
19
21
|
capabilities?: Record<string, unknown>;
|
|
@@ -32,7 +34,7 @@ async function rpc(method: string, params?: Record<string, unknown>): Promise<{
|
|
|
32
34
|
const id = Math.floor(Math.random() * 1e6);
|
|
33
35
|
const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
|
|
34
36
|
if (params !== undefined) req.params = params;
|
|
35
|
-
const res = await driver.post(
|
|
37
|
+
const res = await driver.post(await mcpServerMount(), req);
|
|
36
38
|
return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string } } };
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -83,7 +85,7 @@ describe('mcp-server-tool-roundtrip: behavioral (RFC 0020 §A points 1-2)', () =
|
|
|
83
85
|
if (!registered) return; // host doesn't expose workflow registration
|
|
84
86
|
|
|
85
87
|
const list = await rpc('tools/list');
|
|
86
|
-
if (list.status === 404) return
|
|
88
|
+
if (list.status === 404) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${list.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
87
89
|
expect(list.status, 'tools/list MUST 200').toBe(200);
|
|
88
90
|
const tools = (list.body.result as { tools?: Array<{ name: string }> } | undefined)?.tools ?? [];
|
|
89
91
|
const found = tools.find((t) => t.name === TEST_TOOL_NAME);
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
import { describe, it, expect } from 'vitest';
|
|
14
14
|
import { driver } from '../lib/driver.js';
|
|
15
|
+
import { seamAbsent } from '../lib/soft-skip.js';
|
|
16
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
15
17
|
|
|
16
18
|
interface DiscoveryDoc {
|
|
17
19
|
capabilities?: Record<string, unknown>;
|
|
@@ -30,7 +32,7 @@ async function rpc(method: string, params?: Record<string, unknown>) {
|
|
|
30
32
|
const id = Math.floor(Math.random() * 1e6);
|
|
31
33
|
const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
|
|
32
34
|
if (params !== undefined) req.params = params;
|
|
33
|
-
const res = await driver.post(
|
|
35
|
+
const res = await driver.post(await mcpServerMount(), req);
|
|
34
36
|
return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string; data?: unknown } } };
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -77,7 +79,7 @@ describe('mcp-server-untrusted-args: behavioral (RFC 0020 §D)', () => {
|
|
|
77
79
|
name: TEST_TOOL_NAME,
|
|
78
80
|
arguments: { wrongField: 'no' },
|
|
79
81
|
});
|
|
80
|
-
if (r.status === 404) return;
|
|
82
|
+
if (r.status === 404) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${r.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
81
83
|
expect(r.status, 'JSON-RPC envelope MUST 200').toBe(200);
|
|
82
84
|
expect(
|
|
83
85
|
r.body.error?.code,
|
|
@@ -96,7 +98,7 @@ describe('mcp-server-untrusted-args: behavioral (RFC 0020 §D)', () => {
|
|
|
96
98
|
name: TEST_TOOL_NAME,
|
|
97
99
|
arguments: { text: 'hello' },
|
|
98
100
|
});
|
|
99
|
-
if (r.status === 404) return;
|
|
101
|
+
if (r.status === 404) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${r.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
100
102
|
expect(r.status).toBe(200);
|
|
101
103
|
if (r.body.error) {
|
|
102
104
|
expect(r.body.error.code, 'valid args MUST NOT trigger -32602').not.toBe(-32602);
|
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
|
|
21
21
|
import { describe, it, expect } from 'vitest';
|
|
22
22
|
import { driver } from '../lib/driver.js';
|
|
23
|
+
import { mcpServerMount } from '../lib/mcp-mount.js';
|
|
23
24
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
24
25
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
25
|
-
import { softSkip } from '../lib/soft-skip.js';
|
|
26
|
+
import { softSkip, seamAbsent } from '../lib/soft-skip.js';
|
|
26
27
|
|
|
27
28
|
const PROFILE = 'mcp-2026-07-28';
|
|
28
29
|
const META_V = 'io.modelcontextprotocol/protocolVersion';
|
|
@@ -37,7 +38,7 @@ async function claimsCurrent(): Promise<boolean> {
|
|
|
37
38
|
}
|
|
38
39
|
async function list() {
|
|
39
40
|
const res = await driver.post(
|
|
40
|
-
|
|
41
|
+
await mcpServerMount(),
|
|
41
42
|
{ jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: { [META_V]: '2026-07-28', [META_C]: {}, [META_I]: { name: 'openwop-conformance', version: 'suite' } } } },
|
|
42
43
|
{ headers: { 'MCP-Protocol-Version': '2026-07-28', 'Mcp-Method': 'tools/list' } },
|
|
43
44
|
);
|
|
@@ -49,12 +50,12 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §B — mcp-header-body
|
|
|
49
50
|
if (!behaviorGate(PROFILE, await claimsCurrent())) return;
|
|
50
51
|
const meta = { [META_V]: '2026-07-28', [META_C]: {}, [META_I]: { name: 'openwop-conformance', version: 'suite' } };
|
|
51
52
|
// (a) Mcp-Method header disagrees with the JSON-RPC method
|
|
52
|
-
const m = await driver.post(
|
|
53
|
+
const m = await driver.post(await mcpServerMount(), { jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: meta } }, { headers: { 'MCP-Protocol-Version': '2026-07-28', 'Mcp-Method': 'resources/list' } });
|
|
53
54
|
if (m.status === 404 || m.status === 403) return softSkip('blocked', `MCP server mount /v1/host/sample/mcp answered ${m.status}`);
|
|
54
55
|
expect(m.status, driver.describe('mcp-integration.md §B', 'Mcp-Method MUST equal the body method; disagreement MUST be refused 400 (mcp-header-body-consistent)')).toBe(400);
|
|
55
56
|
expect((m.json as { error?: { code?: number } }).error?.code, driver.describe('mcp-integration.md §B', 'the refusal is HeaderMismatchError -32020')).toBe(-32020);
|
|
56
57
|
// (b) Mcp-Name header disagrees with params.name on tools/call
|
|
57
|
-
const n = await driver.post(
|
|
58
|
+
const n = await driver.post(await mcpServerMount(), { jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'echo', arguments: {}, _meta: meta } }, { headers: { 'MCP-Protocol-Version': '2026-07-28', 'Mcp-Method': 'tools/call', 'Mcp-Name': 'not-echo' } });
|
|
58
59
|
expect(n.status, driver.describe('mcp-integration.md §B', 'Mcp-Name MUST equal params.name; disagreement MUST be refused 400 (mcp-header-body-consistent)')).toBe(400);
|
|
59
60
|
expect((n.json as { error?: { code?: number } }).error?.code, driver.describe('mcp-integration.md §B', 'the refusal is HeaderMismatchError -32020')).toBe(-32020);
|
|
60
61
|
});
|
|
@@ -64,7 +65,7 @@ describe.skipIf(!process.env.OPENWOP_BASE_URL)('RFC 0153 §B — mcp-stateless-r
|
|
|
64
65
|
it('tools/list succeeds with no initialize and no session; result carries resultType + cache hints; two connections agree', async () => {
|
|
65
66
|
if (!behaviorGate(PROFILE, await claimsCurrent())) return;
|
|
66
67
|
const a = await list();
|
|
67
|
-
if (a.status === 404 || a.status === 403) return
|
|
68
|
+
if (a.status === 404 || a.status === 403) return seamAbsent(`host advertises an MCP server mount but the mount (capabilities.mcp.serverUrls[0], else /v1/host/sample/mcp) answered ${a.status} — RFC 0153 §B is unobservable at the path the host itself advertised`);
|
|
68
69
|
expect(a.status, driver.describe('mcp-integration.md §B', 'a core request MUST succeed without a prior initialize or a session header')).toBe(200);
|
|
69
70
|
expect(a.body.error, driver.describe('mcp-integration.md §B', `stateless tools/list MUST NOT error: ${JSON.stringify(a.body.error)}`)).toBeUndefined();
|
|
70
71
|
expect(a.body.result?.resultType, driver.describe('mcp-integration.md §B', 'every current-revision result carries resultType')).toBe('complete');
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import { describe, it, expect } from 'vitest';
|
|
28
28
|
import { driver } from '../lib/driver.js';
|
|
29
|
+
import { seamAbsent } from '../lib/soft-skip.js';
|
|
29
30
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
30
31
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
31
32
|
import { getMcpFakeServer } from '../lib/mcp-fake-server.js';
|
|
@@ -123,8 +124,7 @@ describe('RFC 0153 §A/§B — MCP revision negotiation', () => {
|
|
|
123
124
|
const caps = await mcp();
|
|
124
125
|
server.reset();
|
|
125
126
|
const drive = await driver.post('/v1/host/sample/mcp/invoke', { serverUrl: server.endpoint() });
|
|
126
|
-
if (drive.status === 404 || drive.status === 403) return;
|
|
127
|
-
// Discovery is a promise about behavior. A host that negotiates a revision
|
|
127
|
+
if (drive.status === 404 || drive.status === 403) return seamAbsent(`host advertises mcp version negotiation but the invoke seam /v1/host/sample/mcp/invoke answered ${drive.status} — the host-as-client legs are unobservable (host-sample-test-seams.md)`);
|
|
128
128
|
// it never advertised has made its own discovery document unreliable, which
|
|
129
129
|
// is worse than advertising nothing — a consumer that read it made a
|
|
130
130
|
// decision on a fact that was not true.
|
|
@@ -149,7 +149,7 @@ describe('RFC 0153 §A/§B — MCP revision negotiation', () => {
|
|
|
149
149
|
serverUrl: server.endpoint(),
|
|
150
150
|
requestVersion: '1999-01-01',
|
|
151
151
|
});
|
|
152
|
-
if (drive.status === 404 || drive.status === 403) return;
|
|
152
|
+
if (drive.status === 404 || drive.status === 403) return seamAbsent(`host advertises mcp version negotiation but the invoke seam /v1/host/sample/mcp/invoke answered ${drive.status} — the host-as-client legs are unobservable (host-sample-test-seams.md)`);
|
|
153
153
|
expect(
|
|
154
154
|
drive.status >= 400,
|
|
155
155
|
driver.describe('RFCS/0153 §B', 'an unsupported revision MUST fail rather than silently proceed'),
|