toolfunnel 0.5.0 → 0.6.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 +231 -167
- package/bin/toolfunnel.js +372 -147
- package/docs/KNOWN_BUGS.md +99 -33
- package/docs/MANUAL.pdf +0 -0
- package/docs/design.md +70 -0
- package/docs/packaging.md +115 -115
- package/hooks/hooks.manifest.json +27 -27
- package/hooks/scripts/example-audit.js +21 -21
- package/hooks/scripts/example-deny-dangerous.js +31 -31
- package/mcp/expose.example.json +30 -30
- package/mcp/servers/mock-modern/server.js +147 -0
- package/mcp/servers/mock-upstream/server.js +192 -12
- package/package.json +1 -1
- package/src/auth/config.js +174 -174
- package/src/auth/install.js +181 -181
- package/src/auth/resource-server.js +344 -344
- package/src/core/config-home.js +134 -134
- package/src/core/events.js +105 -105
- package/src/core/hook-engine.js +275 -275
- package/src/core/hook-loader.js +791 -791
- package/src/core/hook-runner.js +346 -346
- package/src/core/logger.js +171 -171
- package/src/core/matcher.js +51 -51
- package/src/core/metrics.js +75 -75
- package/src/core/requires.js +138 -138
- package/src/core/server-config.js +98 -77
- package/src/core/tool-registry.js +180 -180
- package/src/extend/add-hook.md +152 -142
- package/src/extend/add-mcp.md +122 -121
- package/src/extend/configure.md +87 -0
- package/src/extend/create-tool.md +168 -152
- package/src/extend/howto.js +102 -98
- package/src/extend/package.md +187 -187
- package/src/extend/wrap.md +99 -0
- package/src/mcp/aggregator.js +1246 -807
- package/src/mcp/expose-store.js +460 -430
- package/src/mcp/gated-run.js +201 -194
- package/src/mcp/http-transport.js +1311 -1012
- package/src/mcp/mcp-client.js +907 -530
- package/src/mcp/modern.js +372 -0
- package/src/mcp/protocol.js +468 -466
- package/src/mcp/server.js +2688 -1405
- package/src/packages/packager.js +715 -715
- package/src/tools/drift.js +138 -138
- package/src/tools/registry.js +513 -513
- package/src/tools/tool-state.js +170 -104
- package/src/ui/public/app.js +1174 -1055
- package/src/ui/public/index.html +286 -249
- package/src/ui/public/styles.css +535 -535
- package/src/ui/server.js +1348 -1190
- package/tools/scripts/base64.js +139 -139
- package/tools/scripts/danger.js +61 -61
- package/tools/scripts/echo.js +39 -39
- package/tools/scripts/hash.js +101 -101
- package/tools/scripts/json.js +109 -109
- package/tools/scripts/text-stats.js +131 -131
- package/tools/scripts/tf-env.js +43 -43
- package/tools/scripts/tf-hook-add.js +151 -151
- package/tools/scripts/tf-hook-set.js +112 -112
- package/tools/scripts/tf-list.js +192 -192
- package/tools/scripts/tf-log.js +86 -86
- package/tools/scripts/tf-mcp-add.js +117 -117
- package/tools/scripts/tf-mcp-set.js +121 -102
- package/tools/scripts/tf-pack.js +237 -237
- package/tools/scripts/tf-tool-add.js +134 -129
- package/tools/scripts/tf-tool-set.js +142 -142
- package/tools/scripts/tf-wrap.js +103 -0
- package/tools/scripts/uuid.js +102 -102
- package/tools/tools.register.json +227 -216
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* modern.js - the 2026-07-28 ("modern") protocol era, as a pure module.
|
|
5
|
+
*
|
|
6
|
+
* The 2026-07-28 MCP revision is a BREAKING change: it removes the `initialize` handshake,
|
|
7
|
+
* protocol-level sessions and the standalone GET/SSE stream, and replaces them with per-request
|
|
8
|
+
* `_meta` protocol fields, required HTTP routing headers, `server/discover` and
|
|
9
|
+
* `subscriptions/listen`. ToolFunnel 0.6.0 is a DUAL-ERA server: a request carrying modern
|
|
10
|
+
* per-request `_meta` is served under these semantics; an `initialize` request selects the
|
|
11
|
+
* legacy (2024-11-05) path, which is byte-for-byte the 0.5.0 behaviour. The spec's dual-era
|
|
12
|
+
* server rule ("MAY serve both eras concurrently on the same endpoint or process") is exactly
|
|
13
|
+
* what this module enables.
|
|
14
|
+
*
|
|
15
|
+
* This module is PURE and transport-free (the protocol.js philosophy): it owns every modern-era
|
|
16
|
+
* SHAPE - era detection, request validation, header validation, result decoration,
|
|
17
|
+
* `server/discover`, the `subscriptions/listen` message shapes and the modern error codes - and
|
|
18
|
+
* executes nothing. `server.js` (stdio) and `http-transport.js` bind it to their wires.
|
|
19
|
+
*
|
|
20
|
+
* Spec sources (RC, locked 2026-05-21; re-verify against the FINAL spec before release):
|
|
21
|
+
* - modelcontextprotocol.io/specification/draft/changelog
|
|
22
|
+
* - .../draft/basic/transports/streamable-http (headers, validation, error examples)
|
|
23
|
+
* - .../draft/basic/versioning (era model, compatibility matrix)
|
|
24
|
+
* - .../draft/server/discover (DiscoverResult shape)
|
|
25
|
+
* - .../draft/basic/patterns/subscriptions (subscriptions/listen shapes)
|
|
26
|
+
* - .../draft/basic/index (_meta table, resultType, error allocation)
|
|
27
|
+
*
|
|
28
|
+
* CommonJS only. Node built-ins only. No transport. No SDK.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** The modern protocol revision this server implements. */
|
|
32
|
+
const MODERN_PROTOCOL_VERSION = '2026-07-28';
|
|
33
|
+
|
|
34
|
+
/** Reserved `_meta` key prefix + the specific keys the spec defines. */
|
|
35
|
+
const META_KEYS = Object.freeze({
|
|
36
|
+
PROTOCOL_VERSION: 'io.modelcontextprotocol/protocolVersion',
|
|
37
|
+
CLIENT_CAPABILITIES: 'io.modelcontextprotocol/clientCapabilities',
|
|
38
|
+
CLIENT_INFO: 'io.modelcontextprotocol/clientInfo',
|
|
39
|
+
SERVER_INFO: 'io.modelcontextprotocol/serverInfo',
|
|
40
|
+
SUBSCRIPTION_ID: 'io.modelcontextprotocol/subscriptionId',
|
|
41
|
+
LOG_LEVEL: 'io.modelcontextprotocol/logLevel',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/** Modern-era JSON-RPC error codes (-32020..-32099 is the spec-reserved range). */
|
|
45
|
+
const ERR_MODERN = Object.freeze({
|
|
46
|
+
HEADER_MISMATCH: -32020,
|
|
47
|
+
MISSING_REQUIRED_CLIENT_CAPABILITY: -32021,
|
|
48
|
+
UNSUPPORTED_PROTOCOL_VERSION: -32022,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/** JSON-RPC codes shared with the legacy path (kept local so this module stays dependency-free). */
|
|
52
|
+
const ERR_INVALID_PARAMS = -32602;
|
|
53
|
+
const ERR_METHOD_NOT_FOUND = -32601;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Cache hints for the modern CacheableResult surfaces. tools/list can change on any config edit
|
|
57
|
+
* (and we push list_changed when it does), so the freshness hint is short; discover output only
|
|
58
|
+
* changes on upgrade/identity edits, so it can live longer. Both are 'private': this is a
|
|
59
|
+
* per-user gateway whose surface is the USER's visibility matrix - a shared cache must not serve
|
|
60
|
+
* one user's tool surface to another.
|
|
61
|
+
*/
|
|
62
|
+
const CACHE_HINTS = Object.freeze({
|
|
63
|
+
toolsList: Object.freeze({ ttlMs: 60000, cacheScope: 'private' }),
|
|
64
|
+
discover: Object.freeze({ ttlMs: 3600000, cacheScope: 'private' }),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/** The methods whose Mcp-Name header must mirror params.name / params.uri. */
|
|
68
|
+
const NAMED_METHODS = Object.freeze({
|
|
69
|
+
'tools/call': 'name',
|
|
70
|
+
'resources/read': 'uri',
|
|
71
|
+
'prompts/get': 'name',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/** Safely pull params._meta from a parsed JSON-RPC message. Returns null when absent/malformed. */
|
|
75
|
+
function getMeta(msg) {
|
|
76
|
+
const meta = msg && msg.params && msg.params._meta;
|
|
77
|
+
return meta && typeof meta === 'object' && !Array.isArray(meta) ? meta : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Era detection (the spec's dual-era server rule): a request is MODERN iff its params._meta
|
|
82
|
+
* carries the protocolVersion key. `initialize` is ALWAYS legacy - the spec says an initialize
|
|
83
|
+
* request selects legacy semantics, so callers must check the method first (server.js does).
|
|
84
|
+
* @param {object} msg a parsed JSON-RPC message
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
function isModernRequest(msg) {
|
|
88
|
+
const meta = getMeta(msg);
|
|
89
|
+
return !!(meta && typeof meta[META_KEYS.PROTOCOL_VERSION] === 'string');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validate a modern request's `_meta` protocol fields. Returns null when valid, else a
|
|
94
|
+
* descriptor { code, message, data?, httpStatus } the caller turns into a JSON-RPC error
|
|
95
|
+
* (HTTP transports also use httpStatus - the spec mandates 400 for all of these).
|
|
96
|
+
*
|
|
97
|
+
* Spec: protocolVersion + clientCapabilities are MUST; a missing required field is malformed
|
|
98
|
+
* (-32602 + HTTP 400); an unsupported version is -32022 with data.supported/requested.
|
|
99
|
+
* @param {object} msg a parsed JSON-RPC message already known to be modern-shaped
|
|
100
|
+
* @returns {null | { code:number, message:string, data?:object, httpStatus:number }}
|
|
101
|
+
*/
|
|
102
|
+
function validateModernRequest(msg) {
|
|
103
|
+
const meta = getMeta(msg);
|
|
104
|
+
if (!meta) {
|
|
105
|
+
return {
|
|
106
|
+
code: ERR_INVALID_PARAMS,
|
|
107
|
+
message: 'Invalid params: modern requests require params._meta',
|
|
108
|
+
httpStatus: 400,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const version = meta[META_KEYS.PROTOCOL_VERSION];
|
|
112
|
+
if (version !== MODERN_PROTOCOL_VERSION) {
|
|
113
|
+
return {
|
|
114
|
+
code: ERR_MODERN.UNSUPPORTED_PROTOCOL_VERSION,
|
|
115
|
+
message: 'Unsupported protocol version',
|
|
116
|
+
data: { supported: supportedVersions(), requested: version },
|
|
117
|
+
httpStatus: 400,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const caps = meta[META_KEYS.CLIENT_CAPABILITIES];
|
|
121
|
+
if (!caps || typeof caps !== 'object' || Array.isArray(caps)) {
|
|
122
|
+
return {
|
|
123
|
+
code: ERR_INVALID_PARAMS,
|
|
124
|
+
message:
|
|
125
|
+
'Invalid params: _meta["' + META_KEYS.CLIENT_CAPABILITIES + '"] (object) is required',
|
|
126
|
+
httpStatus: 400,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Validate the modern HTTP routing headers against the request body (HTTP transport only -
|
|
134
|
+
* stdio has no headers). Called ONLY for requests already detected as modern. Enforcement is
|
|
135
|
+
* strict per the spec ("These headers are REQUIRED for compliance"):
|
|
136
|
+
* - MCP-Protocol-Version MUST be present and MUST match the body's _meta protocolVersion.
|
|
137
|
+
* - Mcp-Method MUST be present on all requests and MUST match the body method.
|
|
138
|
+
* - Mcp-Name MUST be present on tools/call | resources/read | prompts/get and MUST match
|
|
139
|
+
* params.name / params.uri.
|
|
140
|
+
* Returns null when valid, else { code, message, httpStatus:400 }.
|
|
141
|
+
* @param {object} headers node req.headers (lower-cased keys)
|
|
142
|
+
* @param {object} msg the parsed JSON-RPC body
|
|
143
|
+
* @returns {null | { code:number, message:string, httpStatus:number }}
|
|
144
|
+
*/
|
|
145
|
+
function validateModernHeaders(headers, msg) {
|
|
146
|
+
const h = headers || {};
|
|
147
|
+
const mismatch = (message) => ({ code: ERR_MODERN.HEADER_MISMATCH, message, httpStatus: 400 });
|
|
148
|
+
|
|
149
|
+
const meta = getMeta(msg) || {};
|
|
150
|
+
const bodyVersion = meta[META_KEYS.PROTOCOL_VERSION];
|
|
151
|
+
const headerVersion = h['mcp-protocol-version'];
|
|
152
|
+
if (typeof headerVersion !== 'string' || headerVersion.length === 0) {
|
|
153
|
+
return mismatch('Header mismatch: MCP-Protocol-Version header is required on modern requests');
|
|
154
|
+
}
|
|
155
|
+
if (headerVersion !== bodyVersion) {
|
|
156
|
+
return mismatch(
|
|
157
|
+
`Header mismatch: MCP-Protocol-Version header value '${headerVersion}' does not match body value '${bodyVersion}'`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const headerMethod = h['mcp-method'];
|
|
162
|
+
if (typeof headerMethod !== 'string' || headerMethod.length === 0) {
|
|
163
|
+
return mismatch('Header mismatch: Mcp-Method header is required on modern requests');
|
|
164
|
+
}
|
|
165
|
+
if (headerMethod !== msg.method) {
|
|
166
|
+
return mismatch(
|
|
167
|
+
`Header mismatch: Mcp-Method header value '${headerMethod}' does not match body value '${msg.method}'`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const nameField = NAMED_METHODS[msg.method];
|
|
172
|
+
if (nameField) {
|
|
173
|
+
const bodyName = msg.params ? msg.params[nameField] : undefined;
|
|
174
|
+
const headerName = h['mcp-name'];
|
|
175
|
+
if (typeof headerName !== 'string' || headerName.length === 0) {
|
|
176
|
+
return mismatch(`Header mismatch: Mcp-Name header is required for ${msg.method}`);
|
|
177
|
+
}
|
|
178
|
+
if (headerName !== bodyName) {
|
|
179
|
+
return mismatch(
|
|
180
|
+
`Header mismatch: Mcp-Name header value '${headerName}' does not match body value '${bodyName}'`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The protocol versions this dual-era server speaks (modern first; legacy negotiated via
|
|
188
|
+
* initialize). Kept as a function so the legacy version is injected once by the server, not
|
|
189
|
+
* duplicated here. server.js seeds it via setLegacyVersion(). */
|
|
190
|
+
let _legacyVersion = null;
|
|
191
|
+
function setLegacyVersion(v) {
|
|
192
|
+
if (typeof v === 'string' && v.length > 0) _legacyVersion = v;
|
|
193
|
+
}
|
|
194
|
+
function supportedVersions() {
|
|
195
|
+
return _legacyVersion ? [MODERN_PROTOCOL_VERSION, _legacyVersion] : [MODERN_PROTOCOL_VERSION];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Decorate a result object for the modern era, in place-safe copy:
|
|
200
|
+
* - `resultType: 'complete'` when absent - the preserve-if-present branch below is
|
|
201
|
+
* LOAD-BEARING for Bridge B, whose suspensions arrive here already carrying
|
|
202
|
+
* `resultType: 'input_required'` and must pass through undamaged (the elicitation
|
|
203
|
+
* bridge DOES originate input_required).
|
|
204
|
+
* - `_meta[serverInfo]` (SHOULD on every result).
|
|
205
|
+
* - cache hints (`ttlMs` + `cacheScope`, REQUIRED on the CacheableResult surfaces) when
|
|
206
|
+
* `cacheHints` is given.
|
|
207
|
+
* @param {object} result the legacy-shaped result payload (e.g. { tools: [...] })
|
|
208
|
+
* @param {object} serverInfo { name, version }
|
|
209
|
+
* @param {{ttlMs:number, cacheScope:string}} [cacheHints]
|
|
210
|
+
* @returns {object} a new result object carrying the modern fields
|
|
211
|
+
*/
|
|
212
|
+
function decorateResult(result, serverInfo, cacheHints) {
|
|
213
|
+
const base = result && typeof result === 'object' && !Array.isArray(result) ? result : { value: result };
|
|
214
|
+
const out = Object.assign({}, base);
|
|
215
|
+
if (typeof out.resultType !== 'string') out.resultType = 'complete';
|
|
216
|
+
const meta = Object.assign({}, out._meta);
|
|
217
|
+
if (serverInfo && !meta[META_KEYS.SERVER_INFO]) meta[META_KEYS.SERVER_INFO] = serverInfo;
|
|
218
|
+
out._meta = meta;
|
|
219
|
+
if (cacheHints) {
|
|
220
|
+
if (typeof out.ttlMs !== 'number') out.ttlMs = cacheHints.ttlMs;
|
|
221
|
+
if (typeof out.cacheScope !== 'string') out.cacheScope = cacheHints.cacheScope;
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Build the `server/discover` result (servers MUST implement it; calling it is optional and
|
|
228
|
+
* doubles as the spec's stdio era-probe). Answered for modern AND meta-less probes - the RC
|
|
229
|
+
* does not define the meta-less case, so we answer rather than reject (a probe that errors
|
|
230
|
+
* tells a dual-era client we are legacy-only, which would be a lie).
|
|
231
|
+
* @param {object} serverInfo { name, version }
|
|
232
|
+
* @param {object} capabilities the capability object to advertise (tools etc.)
|
|
233
|
+
* @param {string} [instructions] optional natural-language guidance for LLM clients
|
|
234
|
+
* @returns {object}
|
|
235
|
+
*/
|
|
236
|
+
function discoverResult(serverInfo, capabilities, instructions) {
|
|
237
|
+
const out = {
|
|
238
|
+
resultType: 'complete',
|
|
239
|
+
supportedVersions: supportedVersions(),
|
|
240
|
+
capabilities: capabilities && typeof capabilities === 'object' ? capabilities : { tools: {} },
|
|
241
|
+
_meta: {},
|
|
242
|
+
ttlMs: CACHE_HINTS.discover.ttlMs,
|
|
243
|
+
cacheScope: CACHE_HINTS.discover.cacheScope,
|
|
244
|
+
};
|
|
245
|
+
if (serverInfo) out._meta[META_KEYS.SERVER_INFO] = serverInfo;
|
|
246
|
+
if (typeof instructions === 'string' && instructions.length > 0) out.instructions = instructions;
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Normalise a subscriptions/listen request's notification filter into the subset the SHAPE
|
|
252
|
+
* supports: toolsListChanged, promptsListChanged, resourcesListChanged, resourceSubscriptions
|
|
253
|
+
* (per-URI). This is the SYNTACTIC pass only - whether resourceSubscriptions can actually be
|
|
254
|
+
* HONOURED depends on the connected upstreams (a legacy upstream needs a forwarded
|
|
255
|
+
* resources/subscribe), so the transports refine `agreed` against the aggregator before acking.
|
|
256
|
+
* The ack's `notifications` is what the server actually honours, not what was asked for.
|
|
257
|
+
* @param {object} params the listen request params
|
|
258
|
+
* @returns {{ requested: object, agreed: object }}
|
|
259
|
+
*/
|
|
260
|
+
function normaliseListenFilter(params) {
|
|
261
|
+
const req = params && params.notifications && typeof params.notifications === 'object'
|
|
262
|
+
? params.notifications
|
|
263
|
+
: {};
|
|
264
|
+
const agreed = {};
|
|
265
|
+
if (req.toolsListChanged === true) agreed.toolsListChanged = true;
|
|
266
|
+
if (req.promptsListChanged === true) agreed.promptsListChanged = true;
|
|
267
|
+
if (req.resourcesListChanged === true) agreed.resourcesListChanged = true;
|
|
268
|
+
// resourceSubscriptions: an array of resource URIs the client wants change notifications for.
|
|
269
|
+
if (Array.isArray(req.resourceSubscriptions) && req.resourceSubscriptions.length) {
|
|
270
|
+
agreed.resourceSubscriptions = req.resourceSubscriptions.filter((u) => typeof u === 'string');
|
|
271
|
+
}
|
|
272
|
+
return { requested: req, agreed };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Map a change-notification METHOD to the subscriptions/listen filter channel that gates it. */
|
|
276
|
+
const NOTIFICATION_CHANNEL = Object.freeze({
|
|
277
|
+
'notifications/tools/list_changed': 'toolsListChanged',
|
|
278
|
+
'notifications/prompts/list_changed': 'promptsListChanged',
|
|
279
|
+
'notifications/resources/list_changed': 'resourcesListChanged',
|
|
280
|
+
'notifications/resources/updated': 'resourceSubscriptions',
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Does a change-notification belong to a channel this subscription AGREED to? tools/prompts/
|
|
285
|
+
* resources-list-changed gate on the boolean flag; resources/updated gates on the uri appearing in
|
|
286
|
+
* the agreed resourceSubscriptions list. A notification with no known channel is never delivered on
|
|
287
|
+
* a listen stream (request-scoped notifications ride their own request's stream, not this one).
|
|
288
|
+
* @param {object} notification a JSON-RPC notification
|
|
289
|
+
* @param {object} agreed the agreed filter from normaliseListenFilter
|
|
290
|
+
* @returns {boolean}
|
|
291
|
+
*/
|
|
292
|
+
function notificationMatchesFilter(notification, agreed) {
|
|
293
|
+
if (!notification || typeof notification.method !== 'string' || !agreed) return false;
|
|
294
|
+
const channel = NOTIFICATION_CHANNEL[notification.method];
|
|
295
|
+
if (!channel) return false;
|
|
296
|
+
if (channel === 'resourceSubscriptions') {
|
|
297
|
+
const subs = agreed.resourceSubscriptions;
|
|
298
|
+
if (!Array.isArray(subs) || !subs.length) return false;
|
|
299
|
+
const uri = notification.params && notification.params.uri;
|
|
300
|
+
return typeof uri === 'string' && subs.includes(uri);
|
|
301
|
+
}
|
|
302
|
+
return agreed[channel] === true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The acknowledgment notification a server MUST send as the FIRST message on a listen stream.
|
|
307
|
+
* @param {*} subscriptionId the JSON-RPC id of the subscriptions/listen request
|
|
308
|
+
* @param {object} agreed the honoured filter subset from normaliseListenFilter
|
|
309
|
+
* @returns {object} a JSON-RPC notification object
|
|
310
|
+
*/
|
|
311
|
+
function listenAck(subscriptionId, agreed) {
|
|
312
|
+
return {
|
|
313
|
+
jsonrpc: '2.0',
|
|
314
|
+
method: 'notifications/subscriptions/acknowledged',
|
|
315
|
+
params: {
|
|
316
|
+
_meta: { [META_KEYS.SUBSCRIPTION_ID]: subscriptionId },
|
|
317
|
+
notifications: agreed || {},
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Tag a notification with the subscription id it belongs to (every notification on a listen
|
|
324
|
+
* stream MUST carry it). Returns a copy; the input is not mutated.
|
|
325
|
+
* @param {object} notification a JSON-RPC notification (e.g. tools/list_changed)
|
|
326
|
+
* @param {*} subscriptionId
|
|
327
|
+
* @returns {object}
|
|
328
|
+
*/
|
|
329
|
+
function tagNotification(notification, subscriptionId) {
|
|
330
|
+
const n = notification || {};
|
|
331
|
+
const params = Object.assign({}, n.params);
|
|
332
|
+
params._meta = Object.assign({}, params._meta, { [META_KEYS.SUBSCRIPTION_ID]: subscriptionId });
|
|
333
|
+
return Object.assign({}, n, { params });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* The graceful-closure result for a listen stream (the server ends a subscription by sending a
|
|
338
|
+
* final RESULT to the listen request's id, then closing the stream).
|
|
339
|
+
* @param {*} subscriptionId the listen request's JSON-RPC id
|
|
340
|
+
* @returns {object} a JSON-RPC result object
|
|
341
|
+
*/
|
|
342
|
+
function listenClose(subscriptionId) {
|
|
343
|
+
return {
|
|
344
|
+
jsonrpc: '2.0',
|
|
345
|
+
id: subscriptionId,
|
|
346
|
+
result: {
|
|
347
|
+
resultType: 'complete',
|
|
348
|
+
_meta: { [META_KEYS.SUBSCRIPTION_ID]: subscriptionId },
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
module.exports = {
|
|
354
|
+
MODERN_PROTOCOL_VERSION,
|
|
355
|
+
META_KEYS,
|
|
356
|
+
ERR_MODERN,
|
|
357
|
+
CACHE_HINTS,
|
|
358
|
+
getMeta,
|
|
359
|
+
isModernRequest,
|
|
360
|
+
validateModernRequest,
|
|
361
|
+
validateModernHeaders,
|
|
362
|
+
setLegacyVersion,
|
|
363
|
+
supportedVersions,
|
|
364
|
+
decorateResult,
|
|
365
|
+
discoverResult,
|
|
366
|
+
normaliseListenFilter,
|
|
367
|
+
notificationMatchesFilter,
|
|
368
|
+
NOTIFICATION_CHANNEL,
|
|
369
|
+
listenAck,
|
|
370
|
+
tagNotification,
|
|
371
|
+
listenClose,
|
|
372
|
+
};
|