wiki-formant 0.70.0 → 0.71.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 +1 -0
- package/dist/a2a.d.ts +1 -0
- package/dist/a2a.js +95 -0
- package/dist/conformance.js +7 -0
- package/dist/mcp.d.ts +10 -0
- package/dist/mcp.js +4 -1
- package/dist/store.d.ts +1 -1
- package/dist/well-known.js +12 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -207,6 +207,7 @@ What it gets right:
|
|
|
207
207
|
- **The preflight allow-list includes `Accept` and `Mcp-Protocol-Version`.** One missing entry fails the preflight rather than the POST, which presents as "the server is down".
|
|
208
208
|
- **Batches are capped** (default 20) with a teaching error, because the rate limiter charges one token per HTTP request before the body is parsed. From 2025-06-18 on they are refused outright, because that revision removed them.
|
|
209
209
|
- **Both protocol eras, one endpoint.** A request carrying `io.modelcontextprotocol/protocolVersion` in `params._meta` is served as `2026-07-28`: `server/discover`, per-request version and header validation (`-32022` naming every version spoken, `-32020` on a routing header that disagrees with the body), `resultType` and cache hints on results, and 404 for the methods that revision removed. Everything else is the legacy era, where `initialize` echoes the client's version when it is one of `2025-11-25`, `2025-06-18`, `2025-03-26` or `2024-11-05` and offers the newest otherwise. The spec permits a dual-era server, and it is the only way to adopt the new revision without failing the handshake of every client already in the field.
|
|
210
|
+
- **The A2A call an agent card invites is answered.** Every card names this endpoint as its JSON-RPC `url`, so `message/send` (v0.3) and `SendMessage` (v1.0) get a message back: the result of the tool `answer` names (`{ tool: 'search_wiki', argument: 'query' }`), or `instructions` and the tool names without one. The task methods are `-32001`, since no task is ever created; streaming and push are `-32004`.
|
|
210
211
|
- **The version is negotiated, not asserted.** Every response carries it back in `MCP-Protocol-Version`. Answering a constant is legal and still costs the caller structured output, tool titles and `_meta` without ever saying so.
|
|
211
212
|
- **`Access-Control-Expose-Headers` is set.** Allow-Headers governs what a browser may send, Expose-Headers what it may read — without the second a browser client cannot see `Retry-After` on a 429, and a rate limit presents as a hang.
|
|
212
213
|
- **The rate limit is declared, not wired.** Put `rateLimit: {capacity, refillPerSec}` on the config and `mcpResponse` enforces it before parsing the body, refuses with a JSON-RPC envelope, and states `RateLimit-Limit`/`-Remaining`/`-Reset` on every answer. Four routes had each transcribed that by hand through three differently-named local helpers, which is how a headroom header gets added to one surface and forgotten on the next.
|
package/dist/a2a.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/a2a.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// a2a.ts — the A2A calls an agent card invites, answered on the MCP endpoint.
|
|
2
|
+
//
|
|
3
|
+
// Every card here names the MCP endpoint as its `url` with `preferredTransport:
|
|
4
|
+
// JSONRPC`, which to an A2A client means "send `message/send` here". The
|
|
5
|
+
// endpoint answered -32601, so the card promised an interface the server then
|
|
6
|
+
// refused: A2A clients and the registries that probe for them sent about 140
|
|
7
|
+
// such calls per origin in a fortnight (October 2026) and got nothing back.
|
|
8
|
+
// Now a message is answered with the site's own search, and the task methods
|
|
9
|
+
// say plainly that no task exists, since every answer is a direct message.
|
|
10
|
+
//
|
|
11
|
+
// Both revisions in the field are spoken: v0.3 (`message/send`, a Message with
|
|
12
|
+
// `kind`) and v1.0 (`SendMessage`, a `{ message }` with proto-JSON enums).
|
|
13
|
+
import { McpToolError } from './mcp.js';
|
|
14
|
+
const SEND = { 'message/send': '0.3', SendMessage: '1.0' };
|
|
15
|
+
/** Task methods: none can succeed, because no task is ever created. */
|
|
16
|
+
const TASK = new Set(['tasks/get', 'tasks/cancel', 'tasks/resubscribe', 'GetTask', 'CancelTask', 'SubscribeToTask']);
|
|
17
|
+
/** Everything else either revision defines, which this server declines. */
|
|
18
|
+
const UNSUPPORTED = new Set([
|
|
19
|
+
'message/stream',
|
|
20
|
+
'tasks/pushNotificationConfig/set',
|
|
21
|
+
'tasks/pushNotificationConfig/get',
|
|
22
|
+
'tasks/pushNotificationConfig/list',
|
|
23
|
+
'tasks/pushNotificationConfig/delete',
|
|
24
|
+
'agent/getAuthenticatedExtendedCard',
|
|
25
|
+
'SendStreamingMessage',
|
|
26
|
+
'ListTasks',
|
|
27
|
+
'CreateTaskPushNotificationConfig',
|
|
28
|
+
'GetTaskPushNotificationConfig',
|
|
29
|
+
'ListTaskPushNotificationConfigs',
|
|
30
|
+
'DeleteTaskPushNotificationConfig',
|
|
31
|
+
'GetExtendedAgentCard',
|
|
32
|
+
]);
|
|
33
|
+
/** @internal */
|
|
34
|
+
export const isA2aMethod = (method) => method in SEND || TASK.has(method) || UNSUPPORTED.has(method);
|
|
35
|
+
const error = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
|
|
36
|
+
/** The text of a message's text parts, whichever revision shaped them. */
|
|
37
|
+
function textOf(message) {
|
|
38
|
+
const parts = message?.parts;
|
|
39
|
+
if (!Array.isArray(parts))
|
|
40
|
+
return '';
|
|
41
|
+
return parts
|
|
42
|
+
.map(p => (p && typeof p.text === 'string' ? p.text : ''))
|
|
43
|
+
.join(' ')
|
|
44
|
+
.trim();
|
|
45
|
+
}
|
|
46
|
+
/** What a message gets when there is no search to run: what this server is and how to use it. */
|
|
47
|
+
const orientation = (config) => `${config.instructions}\n\nThis endpoint also speaks MCP (JSON-RPC over POST): send "initialize", then "tools/list". Tools: ${config.tools.map(t => t.name).join(', ')}.`;
|
|
48
|
+
async function reply(text, config) {
|
|
49
|
+
const tool = config.answer && config.tools.find(t => t.name === config.answer.tool);
|
|
50
|
+
if (!text || !tool)
|
|
51
|
+
return { text: orientation(config) };
|
|
52
|
+
try {
|
|
53
|
+
const out = await tool.handler({ [config.answer.argument]: text }, {
|
|
54
|
+
meta: {},
|
|
55
|
+
setMeta: () => { },
|
|
56
|
+
protocolVersion: '2025-11-25',
|
|
57
|
+
});
|
|
58
|
+
const lead = `${config.serverInfo.name} ${tool.name} for ${JSON.stringify(text)}. For more, call this endpoint over MCP: "initialize", then "tools/list".`;
|
|
59
|
+
if (typeof out === 'string')
|
|
60
|
+
return { text: `${lead}\n\n${out}` };
|
|
61
|
+
// The text twin stays beside the data, as it does on an MCP tool result:
|
|
62
|
+
// a client that reads only text parts still has to be able to read the answer.
|
|
63
|
+
return { text: `${lead}\n\n${JSON.stringify(out, null, 2)}`, ...(out && typeof out === 'object' && !Array.isArray(out) ? { data: out } : {}) };
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (err instanceof McpToolError)
|
|
67
|
+
return { text: `${err.message}\n\n${orientation(config)}` };
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Answer one A2A call. Only reached for a method `isA2aMethod` accepts. @internal */
|
|
72
|
+
export async function handleA2a(req, config) {
|
|
73
|
+
const { id, method } = req;
|
|
74
|
+
if (TASK.has(method)) {
|
|
75
|
+
return error(id, -32001, 'Task not found: this agent keeps no tasks. Every message it is sent is answered directly with a message.');
|
|
76
|
+
}
|
|
77
|
+
const revision = SEND[method];
|
|
78
|
+
if (!revision) {
|
|
79
|
+
const send = method.includes('/') ? 'message/send' : 'SendMessage';
|
|
80
|
+
return error(id, -32004, `Unsupported operation: "${method}". This agent answers ${send} only, without streaming or push notifications.`);
|
|
81
|
+
}
|
|
82
|
+
const message = req.params?.message;
|
|
83
|
+
if (!message || typeof message !== 'object') {
|
|
84
|
+
return error(id, -32602, 'Missing params.message: send a message with at least one text part.');
|
|
85
|
+
}
|
|
86
|
+
const { text, data } = await reply(textOf(message), config);
|
|
87
|
+
const contextId = typeof message.contextId === 'string' ? message.contextId : crypto.randomUUID();
|
|
88
|
+
const messageId = crypto.randomUUID();
|
|
89
|
+
if (revision === '1.0') {
|
|
90
|
+
const parts = [{ text }, ...(data ? [{ data }] : [])];
|
|
91
|
+
return { jsonrpc: '2.0', id, result: { message: { messageId, contextId, role: 'ROLE_AGENT', parts } } };
|
|
92
|
+
}
|
|
93
|
+
const parts = [{ kind: 'text', text }, ...(data ? [{ kind: 'data', data }] : [])];
|
|
94
|
+
return { jsonrpc: '2.0', id, result: { kind: 'message', messageId, contextId, role: 'agent', parts } };
|
|
95
|
+
}
|
package/dist/conformance.js
CHANGED
|
@@ -294,6 +294,13 @@ export async function agentCardParity(t) {
|
|
|
294
294
|
const probe = await rawPost(endpoint, { jsonrpc: '2.0', id: 1, method: 'ping' });
|
|
295
295
|
t.recordCall();
|
|
296
296
|
t.check('agent card url is callable', (probe.headers.get('content-type') ?? '').includes('json'), `POST ${endpoint} -> ${probe.status} ${probe.headers.get('content-type')}`);
|
|
297
|
+
// Callable is not the same as answering. A `JSONRPC` url invites A2A's own
|
|
298
|
+
// first call, and a -32601 to it refuses the interface the card names.
|
|
299
|
+
const send = await t.rpcAt(endpoint, 'message/send', {
|
|
300
|
+
message: { kind: 'message', role: 'user', messageId: 'conformance', parts: [{ kind: 'text', text: String(a.name ?? 'help') }] },
|
|
301
|
+
});
|
|
302
|
+
const reply = send.result;
|
|
303
|
+
t.check('agent card url answers A2A', reply?.kind === 'message' && typeof reply.parts?.[0]?.text === 'string', reply ? `message/send -> ${reply.parts?.length} part(s)` : `message/send -> ${JSON.stringify(send.error)}`);
|
|
297
304
|
}
|
|
298
305
|
/**
|
|
299
306
|
* Any surface an agent recrawls should answer a conditional GET with a 304.
|
package/dist/mcp.d.ts
CHANGED
|
@@ -179,6 +179,16 @@ export interface McpServerConfig {
|
|
|
179
179
|
* clicks when they do not have one yet.
|
|
180
180
|
*/
|
|
181
181
|
prompts?: McpPrompt[];
|
|
182
|
+
/**
|
|
183
|
+
* The tool an A2A `message/send` is answered with, and the argument its text
|
|
184
|
+
* fills: usually the search tool and its query. Without it a message is
|
|
185
|
+
* answered with `instructions` and the tool names, which is still an answer
|
|
186
|
+
* where -32601 was a refusal of the interface the agent card names.
|
|
187
|
+
*/
|
|
188
|
+
answer?: {
|
|
189
|
+
tool: string;
|
|
190
|
+
argument: string;
|
|
191
|
+
};
|
|
182
192
|
/** Appended to the GET refusal so a browser that lands here learns where to go. */
|
|
183
193
|
docsUrl?: string;
|
|
184
194
|
/**
|
package/dist/mcp.js
CHANGED
|
@@ -66,6 +66,7 @@ function requestProtocol(request) {
|
|
|
66
66
|
}
|
|
67
67
|
/** JSON-RPC batching was removed in 2025-06-18 and stays removed; it is legal only below that. */
|
|
68
68
|
const allowsBatch = (v) => v === '2025-03-26' || v === '2024-11-05';
|
|
69
|
+
import { handleA2a, isA2aMethod } from './a2a.js';
|
|
69
70
|
import { clientKey, rateLimit, rateLimitHeaders, withRateLimit } from './rate-limit.js';
|
|
70
71
|
/**
|
|
71
72
|
* A tool as every surface advertises it — `tools/list`, the agent card and
|
|
@@ -418,6 +419,8 @@ async function handleRpc(req, config, dispatch) {
|
|
|
418
419
|
}
|
|
419
420
|
}
|
|
420
421
|
default:
|
|
422
|
+
if (isA2aMethod(method))
|
|
423
|
+
return await handleA2a(req, config);
|
|
421
424
|
return methodNotFound(id, method, config, dispatch.era);
|
|
422
425
|
}
|
|
423
426
|
}
|
|
@@ -452,7 +455,7 @@ export async function handleMcp(body, config, dispatch = { protocolVersion: ASSU
|
|
|
452
455
|
export const MCP_CORS = {
|
|
453
456
|
'Access-Control-Allow-Origin': '*',
|
|
454
457
|
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
455
|
-
'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization, Mcp-Session-Id, Mcp-Protocol-Version, Mcp-Method, Mcp-Name, Last-Event-ID',
|
|
458
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization, Mcp-Session-Id, Mcp-Protocol-Version, Mcp-Method, Mcp-Name, Last-Event-ID, A2A-Version',
|
|
456
459
|
// Allow-Headers governs what a browser may send; without Expose-Headers it
|
|
457
460
|
// may read none of what comes back. A browser client could not see the
|
|
458
461
|
// negotiated version, and could not see `Retry-After` on the 429 telling it
|
package/dist/store.d.ts
CHANGED
|
@@ -147,8 +147,8 @@ export declare function wikiStore<B extends DiffBlock = DiffBlock>(o: WikiStoreO
|
|
|
147
147
|
items: {
|
|
148
148
|
content: B[];
|
|
149
149
|
title: string;
|
|
150
|
-
metadata: PageMetadata;
|
|
151
150
|
id: string;
|
|
151
|
+
metadata: PageMetadata;
|
|
152
152
|
tagPath: string;
|
|
153
153
|
slug: string;
|
|
154
154
|
siteId: string;
|
package/dist/well-known.js
CHANGED
|
@@ -84,10 +84,20 @@ export function agentCard(config) {
|
|
|
84
84
|
// The endpoint, not the homepage. A card's `url` is where a client sends
|
|
85
85
|
// its first call, and every card here pointed at an HTML page — so a client
|
|
86
86
|
// that believed the card got markup back, or a 405. MCP is JSON-RPC over
|
|
87
|
-
// HTTP POST,
|
|
88
|
-
//
|
|
87
|
+
// HTTP POST, and the endpoint answers A2A's `message/send` (v0.3) and
|
|
88
|
+
// `SendMessage` (v1.0) from the site's own search (`McpServerConfig.answer`).
|
|
89
89
|
url: endpoint,
|
|
90
90
|
preferredTransport: 'JSONRPC',
|
|
91
|
+
// v1.0 reads the interfaces from here and ignores `url`; v0.3 the reverse.
|
|
92
|
+
...(mcpEndpoint === null
|
|
93
|
+
? {}
|
|
94
|
+
: {
|
|
95
|
+
supportedInterfaces: ['1.0', '0.3'].map(protocolVersion => ({
|
|
96
|
+
url: endpoint,
|
|
97
|
+
protocolBinding: 'JSONRPC',
|
|
98
|
+
protocolVersion,
|
|
99
|
+
})),
|
|
100
|
+
}),
|
|
91
101
|
version,
|
|
92
102
|
capabilities: { streaming: false, pushNotifications: false },
|
|
93
103
|
skills,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-formant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.71.0",
|
|
4
4
|
"description": "The portable half of a wiki: derived taxonomy and facet controls, a version-negotiating MCP transport, markdown twins, block rendering, a rich-text editor engine, conditional-GET plumbing, and the headers and CSP every origin sends.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|