Skip to content

Commit b73e7c0

Browse files
committed
test(js): cover chatOnlyManifest + publish() resources/prompts parity
5 tests against the real publish() entry point and a local ws.WebSocketServer that captures the register-publisher envelope: - chatOnlyManifest defaults resources/prompts to [] - chatOnlyManifest threads resources verbatim - chatOnlyManifest threads prompts verbatim - chat capability schema matches Python's properties block - publish() ships resources + prompts in the registered manifest Pre-fix mutation verification: every test fails at tsc (the resources/prompts fields don't exist on Manifest or PublishOptions), so a regression is caught at compile time, not just at runtime.
1 parent 46e9b39 commit b73e7c0

2 files changed

Lines changed: 126 additions & 1 deletion

File tree

js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
],
2727
"scripts": {
2828
"build": "tsc -p tsconfig.json",
29-
"test": "tsc -p tsconfig.test.json && node --test dist-test/test/manifest.test.js dist-test/test/protocol.test.js dist-test/test/client.test.js dist-test/test/client_url.test.js dist-test/test/client_lifecycle.test.js dist-test/test/client_stream.test.js dist-test/test/client_handle_chat.test.js"
29+
"test": "tsc -p tsconfig.test.json && node --test dist-test/test/manifest.test.js dist-test/test/manifest_mcp.test.js dist-test/test/protocol.test.js dist-test/test/client.test.js dist-test/test/client_url.test.js dist-test/test/client_lifecycle.test.js dist-test/test/client_stream.test.js dist-test/test/client_handle_chat.test.js"
3030
},
3131
"dependencies": {
3232
"ws": "^8.18.0"

js/test/manifest_mcp.test.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/**
2+
* chatOnlyManifest + publish() resources/prompts parity with Python.
3+
*
4+
* Regression for: the JS Manifest interface and chatOnlyManifest builder
5+
* had no resources/prompts fields, and PublishOptions had no way to declare
6+
* them. mcp_server iterates `manifest['resources']`/`manifest['prompts']`
7+
* to answer resources/list, prompts/list etc — so a JS publisher's manifest
8+
* silently exposed nothing on the MCP surface even when the author intended
9+
* to declare resources/prompts (no compile error, no runtime error, just
10+
* missing functionality vs the equivalent Python publisher).
11+
*
12+
* Also covers the chat-capability schema parity — Python emits a full
13+
* properties block (messages/model/temperature/max_tokens), JS was emitting
14+
* `{type: 'object'}` only.
15+
*/
16+
import { describe, it, before, after } from 'node:test';
17+
import assert from 'node:assert/strict';
18+
import { WebSocketServer } from 'ws';
19+
import type { AddressInfo } from 'node:net';
20+
import { chatOnlyManifest } from '../src/manifest.js';
21+
import { publish } from '../src/client.js';
22+
23+
describe('chatOnlyManifest MCP surface parity with Python', () => {
24+
it('always emits empty resources + prompts arrays when none provided', () => {
25+
const m = chatOnlyManifest({ name: 'x', description: '' });
26+
assert.deepEqual(m.resources, []);
27+
assert.deepEqual(m.prompts, []);
28+
});
29+
30+
it('threads resources through verbatim', () => {
31+
const resources = [
32+
{ uri: 'file://hello.txt', name: 'hello', content: 'world' },
33+
{ uri: 'file://b.json', name: 'b', mimeType: 'application/json', content: '{}' },
34+
];
35+
const m = chatOnlyManifest({ name: 'x', description: '', resources });
36+
assert.deepEqual(m.resources, resources);
37+
});
38+
39+
it('threads prompts through verbatim', () => {
40+
const prompts = [
41+
{
42+
name: 'greet',
43+
description: 'say hi',
44+
arguments: [{ name: 'who', required: true }],
45+
messages: [{ role: 'user', content: 'hi {who}' }],
46+
},
47+
];
48+
const m = chatOnlyManifest({ name: 'x', description: '', prompts });
49+
assert.deepEqual(m.prompts, prompts);
50+
});
51+
52+
it('chat capability schema matches Python (full properties block)', () => {
53+
const m = chatOnlyManifest({ name: 'x', description: '' });
54+
const chat = m.capabilities?.find((c) => c.name === 'chat');
55+
assert.ok(chat, 'chat capability present');
56+
assert.deepEqual(chat.schema, {
57+
type: 'object',
58+
properties: {
59+
messages: { type: 'array' },
60+
model: { type: 'string' },
61+
temperature: { type: 'number' },
62+
max_tokens: { type: 'integer' },
63+
},
64+
});
65+
});
66+
});
67+
68+
describe('publish() threads resources/prompts into the registered manifest', () => {
69+
let wss: WebSocketServer;
70+
let port: number;
71+
let closeServer: () => Promise<void>;
72+
let receivedManifest: Record<string, unknown> | null = null;
73+
74+
before(async () => {
75+
wss = new WebSocketServer({ port: 0 });
76+
await new Promise<void>((r) => wss.on('listening', r));
77+
port = (wss.address() as AddressInfo).port;
78+
wss.on('connection', (ws) => {
79+
ws.on('message', (raw) => {
80+
const env = JSON.parse(String(raw));
81+
if (env.type === 'register-publisher') {
82+
receivedManifest = env.payload.manifest as Record<string, unknown>;
83+
ws.send(
84+
JSON.stringify({
85+
type: 'registered',
86+
request_id: env.request_id,
87+
payload: { name: 'test-pub', base_url: '', api_key: 'zk_test' },
88+
}),
89+
);
90+
}
91+
});
92+
});
93+
closeServer = () => new Promise<void>((r) => wss.close(() => r()));
94+
});
95+
96+
after(async () => {
97+
await closeServer();
98+
});
99+
100+
it('register-publisher carries the resources + prompts arrays', async () => {
101+
const resources = [{ uri: 'file://r.txt', name: 'r', content: 'rr' }];
102+
const prompts = [{ name: 'p', messages: [{ role: 'user', content: 'hi' }] }];
103+
104+
const pub = publish({
105+
name: 'test-pub',
106+
description: 'parity probe',
107+
hubUrl: `http://127.0.0.1:${port}`,
108+
apiKey: 'zk_test',
109+
chatHandler: async () => 'hi',
110+
resources,
111+
prompts,
112+
});
113+
114+
// Wait for register-publisher round-trip
115+
for (let i = 0; i < 50; i++) {
116+
if (receivedManifest) break;
117+
await new Promise<void>((r) => setTimeout(r, 20));
118+
}
119+
assert.ok(receivedManifest, 'register-publisher manifest received');
120+
assert.deepEqual(receivedManifest!.resources, resources);
121+
assert.deepEqual(receivedManifest!.prompts, prompts);
122+
123+
await pub.stop();
124+
});
125+
});

0 commit comments

Comments
 (0)