crawlforge-mcp-server 5.10.0 → 6.0.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.
@@ -37,7 +37,7 @@ const ChangeTrackingSchema = z.object({
37
37
  moderate: z.number().min(0).max(1).default(0.3),
38
38
  major: z.number().min(0).max(1).default(0.7)
39
39
  }).optional()
40
- }).optional().default({})
40
+ }).optional().prefault({})
41
41
  });
42
42
 
43
43
  const ChangeComparisonSchema = z.object({
@@ -5,8 +5,58 @@
5
5
  * expensive or ambiguous operations. Falls back gracefully when the
6
6
  * MCP client does not support elicitation.
7
7
  *
8
- * MCP Spec 2025-11-25: client/elicit request with requestedSchema
8
+ * The request goes out as a 2025-era server→client `elicitation/create`,
9
+ * awaited inline in the middle of a tool's work. Two things the SDK does NOT
10
+ * do for us, and which `supported` therefore decides before we send:
11
+ *
12
+ * - `Server.elicitInput()` refuses a client that declared a bare
13
+ * `elicitation: {}` (it demands `elicitation.form`), even though the SDK's
14
+ * own capability rule counts a bare declaration as form-capable — that is
15
+ * the pre-mode 2025 meaning, and it is what the SDK's legacy shim and its
16
+ * 2026 seam both apply. We send through `Server.request()`, which applies
17
+ * that same lenient rule, so those clients get their prompt.
18
+ *
19
+ * - Both `elicitInput()` and `request()` throw on a 2026-07-28-era
20
+ * connection: that revision has no server→client request channel at all.
21
+ * There is no inline substitute — the replacement is an `input_required`
22
+ * result RETURNED by a tools/call handler, which this helper cannot do
23
+ * from the middle of a tool's execution. So `supported` reports false and
24
+ * the operation proceeds unasked, exactly as it does for a client with no
25
+ * elicitation capability. A confirmation prompt is a nicety; failing the
26
+ * call is not an acceptable substitute. See docs/mcp-spec-adoption.md.
27
+ */
28
+
29
+ /**
30
+ * First revision of the modern protocol era. Revisions are ISO dates, so
31
+ * lexicographic comparison orders them chronologically (the SDK's own rule).
9
32
  */
33
+ const FIRST_MODERN_PROTOCOL_VERSION = '2026-07-28';
34
+
35
+ /** The one-boolean schema a confirmation asks with. */
36
+ const CONFIRM_SCHEMA = {
37
+ type: 'object',
38
+ properties: {
39
+ confirmed: {
40
+ type: 'boolean',
41
+ title: 'Proceed?',
42
+ description: 'Confirm to proceed with the operation',
43
+ },
44
+ },
45
+ required: ['confirmed'],
46
+ };
47
+
48
+ /**
49
+ * Whether the client's declared capabilities cover FORM elicitation, by the
50
+ * SDK's own rule: `elicitation.form` counts, and so does a bare `elicitation`
51
+ * declaration naming neither mode (the pre-mode 2025 meaning). A client that
52
+ * declared only `elicitation.url` has not declared form support.
53
+ */
54
+ function formElicitationDeclared(caps) {
55
+ const elicitation = caps?.elicitation;
56
+ if (!elicitation) return false;
57
+ if (elicitation.form !== undefined) return true;
58
+ return elicitation.url === undefined;
59
+ }
10
60
 
11
61
  export class ElicitationHelper {
12
62
  /**
@@ -20,22 +70,35 @@ export class ElicitationHelper {
20
70
  }
21
71
 
22
72
  /**
23
- * Whether the connected MCP client supports elicitation.
73
+ * Whether an inline elicitation round trip will actually reach the user.
24
74
  * @returns {boolean}
25
75
  */
26
76
  get supported() {
27
77
  const server = this._mcpServer?.server;
28
- // The MCP SDK exposes elicitation via Server.elicitInput(); it is only
29
- // usable when the connected CLIENT advertised the `elicitation` capability.
30
- if (typeof server?.elicitInput !== 'function') return false;
78
+ if (typeof server?.request !== 'function') return false;
31
79
  try {
32
- const caps = server.getClientCapabilities?.();
33
- return !!caps?.elicitation;
80
+ // No server→client request channel exists on the 2026-07-28 era.
81
+ const negotiated = server.getNegotiatedProtocolVersion?.();
82
+ if (typeof negotiated === 'string' && negotiated >= FIRST_MODERN_PROTOCOL_VERSION) return false;
83
+ return formElicitationDeclared(server.getClientCapabilities?.());
34
84
  } catch {
35
85
  return false;
36
86
  }
37
87
  }
38
88
 
89
+ /**
90
+ * Send one form-mode `elicitation/create` and return the ElicitResult.
91
+ * This is byte-for-byte the message `Server.elicitInput()` sends; it just
92
+ * does not impose that method's stricter `elicitation.form` gate.
93
+ * @private
94
+ */
95
+ async _elicit(message, requestedSchema) {
96
+ return this._mcpServer.server.request({
97
+ method: 'elicitation/create',
98
+ params: { message, requestedSchema, mode: 'form' },
99
+ });
100
+ }
101
+
39
102
  /**
40
103
  * Ask for user confirmation before proceeding with an expensive operation.
41
104
  * Returns true if confirmed (or if elicitation is unsupported — fail-open
@@ -57,20 +120,7 @@ export class ElicitationHelper {
57
120
  .join('\n');
58
121
  const fullMessage = detailLines ? `${message}\n\n${detailLines}` : message;
59
122
 
60
- const result = await this._mcpServer.server.elicitInput({
61
- message: fullMessage,
62
- requestedSchema: {
63
- type: 'object',
64
- properties: {
65
- confirmed: {
66
- type: 'boolean',
67
- title: 'Proceed?',
68
- description: 'Confirm to proceed with the operation',
69
- },
70
- },
71
- required: ['confirmed'],
72
- },
73
- });
123
+ const result = await this._elicit(fullMessage, CONFIRM_SCHEMA);
74
124
 
75
125
  // Only an explicit accept + confirmed=true proceeds; decline/cancel = stop.
76
126
  return result?.action === 'accept' && result?.content?.confirmed === true;
@@ -97,23 +147,22 @@ export class ElicitationHelper {
97
147
  }
98
148
 
99
149
  try {
100
- const result = await this._mcpServer.server.elicitInput({
101
- message,
102
- requestedSchema: {
103
- type: 'object',
104
- properties: {
105
- [fieldName]: {
106
- type: 'string',
107
- title: fieldName,
108
- description: fieldDescription,
109
- ...(defaultValue ? { default: defaultValue } : {}),
110
- },
150
+ const result = await this._elicit(message, {
151
+ type: 'object',
152
+ properties: {
153
+ [fieldName]: {
154
+ type: 'string',
155
+ title: fieldName,
156
+ description: fieldDescription,
157
+ ...(defaultValue ? { default: defaultValue } : {}),
111
158
  },
112
- required: [fieldName],
113
159
  },
160
+ required: [fieldName],
114
161
  });
115
162
 
116
- if (result?.action === 'accept' && result?.content?.[fieldName] != null) {
163
+ // The answer is client-supplied and no longer schema-checked by the SDK
164
+ // on this path, so hold it to the type we asked for.
165
+ if (result?.action === 'accept' && typeof result?.content?.[fieldName] === 'string') {
117
166
  return result.content[fieldName];
118
167
  }
119
168
  return defaultValue || null;
@@ -7,7 +7,9 @@
7
7
  * Fallback chain (applied in resolveCompletion):
8
8
  * 1. Ollama (local, no API key needed)
9
9
  * 2. Server-side API key (OPENAI_API_KEY / ANTHROPIC_API_KEY)
10
- * 3. MCP sampling request to client
10
+ * 3. MCP sampling request to client — DEPRECATED in MCP revision 2026-07-28
11
+ * (SEP-2577); removal on or after 2027-07-28. Emits a one-line stderr
12
+ * deprecation notice when it serves a completion.
11
13
  * 4. Error
12
14
  */
13
15
 
@@ -157,7 +159,11 @@ export class SamplingClient {
157
159
  includeContext: 'none',
158
160
  });
159
161
  const text = samplingResult?.content?.text || '';
160
- if (text) return { text, provider: 'sampling' };
162
+ if (text) {
163
+ // stderr, never stdout — stdout is the JSON-RPC stream on stdio.
164
+ console.error('[deprecation] MCP sampling served this completion. Sampling was deprecated in MCP revision 2026-07-28 (SEP-2577); CrawlForge removes this fallback on or after 2027-07-28. Run Ollama or set OPENAI_API_KEY / ANTHROPIC_API_KEY.');
165
+ return { text, provider: 'sampling' };
166
+ }
161
167
  } catch (_samplingErr) {
162
168
  // Sampling not supported or failed
163
169
  }
@@ -28,7 +28,7 @@ const ContentAnalyzerSchema = z.object({
28
28
  maxKeywords: z.number().min(1).max(50).default(15),
29
29
  includeReadabilityMetrics: z.boolean().default(true),
30
30
  includeSentiment: z.boolean().default(true)
31
- }).optional().default({})
31
+ }).optional().prefault({})
32
32
  });
33
33
 
34
34
  const AnalysisResult = z.object({
@@ -74,7 +74,7 @@ const BrowserProcessorSchema = z.object({
74
74
  enableTimezoneSpoof: z.boolean().default(true),
75
75
  enableGeoLocationSpoof: z.boolean().default(true)
76
76
  }).optional()
77
- }).optional().default({})
77
+ }).optional().prefault({})
78
78
  });
79
79
 
80
80
  const BrowserResult = z.object({
@@ -20,7 +20,7 @@ const ContentProcessorSchema = z.object({
20
20
  removeBoilerplate: z.boolean().default(true),
21
21
  preserveImageInfo: z.boolean().default(true),
22
22
  extractMetadata: z.boolean().default(true)
23
- }).optional().default({})
23
+ }).optional().prefault({})
24
24
  });
25
25
 
26
26
  const ReadabilityResult = z.object({
@@ -31,8 +31,8 @@ const PDFProcessorSchema = z.object({
31
31
  parseOptions: z.object({
32
32
  normalizeWhitespace: z.boolean().default(true),
33
33
  disableCombineTextItems: z.boolean().default(false)
34
- }).optional().default({})
35
- }).optional().default({})
34
+ }).optional().prefault({})
35
+ }).optional().prefault({})
36
36
  });
37
37
 
38
38
  const PDFResult = z.object({
@@ -10,7 +10,7 @@
10
10
  * validates `structuredContent` against the schema; legacy clients keep
11
11
  * reading the JSON-stringified `content` for backward compatibility.
12
12
  *
13
- * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
13
+ * @param {import('@modelcontextprotocol/server').McpServer} server
14
14
  * @param {Function} withAuth — from makeWithAuth() in src/server/withAuth.js
15
15
  * @param {Object} descriptor
16
16
  * @param {string} descriptor.name — tool name (MCP identifier)
@@ -1,19 +1,22 @@
1
1
  /**
2
2
  * specHygiene — Phase 6 protocol-hygiene wrapper.
3
3
  *
4
- * Applies three MCP wire-level upgrades to an already-registered McpServer
4
+ * Applies four MCP wire-level upgrades to an already-registered McpServer
5
5
  * without changing how tools/prompts are declared anywhere else:
6
6
  *
7
7
  * 1. JSON Schema 2020-12 dialect stamping on every tool's inputSchema /
8
- * outputSchema (the SDK's zod-to-json-schema conversion still emits
9
- * draft-07-style `definitions` / `#/definitions/...` refs).
8
+ * outputSchema. Under the v2 SDK + zod 4 the conversion already emits
9
+ * 2020-12 with `$defs`, so the `definitions` -> `$defs` rewrite below is
10
+ * now defensive rather than load-bearing; it is kept because the stamping
11
+ * and ordering passes still walk the same tree (Phase 4.1 kept behaviour
12
+ * identical — removing the rewrite is a separate, verifiable change).
10
13
  * 2. Deterministic (alphabetical) tool ordering in tools/list, so clients
11
14
  * that prompt-cache tools/list get stable hits across restarts.
12
15
  * 3. SEP-973 icons metadata on every tool/prompt lacking one.
13
16
  * 4. SEP-2549-style cache hints on tools/call results for a fixed
14
17
  * allowlist of read-only, idempotent tools (see note below).
15
18
  *
16
- * Wiring: SDK 1.30 has no plugin hook for this, so applySpecHygiene() must be
19
+ * Wiring: the SDK has no plugin hook for this, so applySpecHygiene() must be
17
20
  * called once, after all server.registerTool()/registerPrompt() calls and
18
21
  * before transport.connect(). It reaches into `server.server` (the
19
22
  * underlying Protocol), captures the ListTools/CallTool/ListPrompts handlers
@@ -22,26 +25,18 @@
22
25
  * handler for the same method (confirmed from the SDK source: only
23
26
  * `assertCanSetRequestHandler`, called by McpServer's own registration path,
24
27
  * throws on a pre-existing handler; `setRequestHandler` itself does not).
28
+ * In v2 `setRequestHandler` takes a method string rather than a Zod schema.
25
29
  *
26
- * SEP-2549 note: as specified (2026-07-28 RC), `ttlMs` / `cacheScope`
27
- * ("public"|"private") are defined on tools/list, prompts/list,
28
- * resources/list, resources/read and resources/templates/list results
29
- * there is no CacheableResult for tools/call. Since this deliverable asks
30
- * for tools/call cache hints, the same two field names are carried into a
31
- * namespaced `_meta` key following the SDK's own
32
- * `io.modelcontextprotocol/<name>` convention (see RELATED_TASK_META_KEY in
33
- * @modelcontextprotocol/sdk/types.js):
30
+ * SEP-2549 note: as specified (2026-07-28), `ttlMs` / `cacheScope`
31
+ * ("public"|"private") are defined on server/discover results — there is no
32
+ * CacheableResult for tools/call. Since this deliverable asks for tools/call
33
+ * cache hints, the same two field names are carried into a namespaced `_meta`
34
+ * key following the SDK's own `io.modelcontextprotocol/<name>` convention:
34
35
  * result._meta["io.modelcontextprotocol/cacheable"] = { ttlMs, cacheScope }
35
36
  * This is a documented adaptation, not a key confirmed by the spec text for
36
37
  * tools/call — revisit if/when a SEP defines call-result caching explicitly.
37
38
  */
38
39
 
39
- import {
40
- ListToolsRequestSchema,
41
- CallToolRequestSchema,
42
- ListPromptsRequestSchema
43
- } from '@modelcontextprotocol/sdk/types.js';
44
-
45
40
  const APPLIED = Symbol('crawlforge.specHygiene.applied');
46
41
 
47
42
  const JSON_SCHEMA_2020_12 = 'https://json-schema.org/draft/2020-12/schema';
@@ -158,7 +153,7 @@ function wrapToolsCall(innerHandler, cacheableTools) {
158
153
  * tools/prompts are registered and before transport.connect(). Idempotent:
159
154
  * a second call on the same server is a no-op.
160
155
  *
161
- * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
156
+ * @param {import('@modelcontextprotocol/server').McpServer} server
162
157
  * @param {object} [overrides]
163
158
  * @param {{src:string,mimeType?:string,sizes?:string[]}} [overrides.icon] — default icon injected into tools/prompts lacking one
164
159
  * @param {Record<string,{ttlMs:number,cacheScope:'public'|'private'}>} [overrides.cacheableTools] — tool-name -> cache hint map for tools/call
@@ -177,16 +172,16 @@ export function applySpecHygiene(server, overrides = {}) {
177
172
 
178
173
  const innerToolsList = protocol._requestHandlers.get('tools/list');
179
174
  if (innerToolsList) {
180
- protocol.setRequestHandler(ListToolsRequestSchema, wrapToolsList(innerToolsList, icon));
175
+ protocol.setRequestHandler('tools/list', wrapToolsList(innerToolsList, icon));
181
176
  }
182
177
 
183
178
  const innerToolsCall = protocol._requestHandlers.get('tools/call');
184
179
  if (innerToolsCall) {
185
- protocol.setRequestHandler(CallToolRequestSchema, wrapToolsCall(innerToolsCall, cacheableTools));
180
+ protocol.setRequestHandler('tools/call', wrapToolsCall(innerToolsCall, cacheableTools));
186
181
  }
187
182
 
188
183
  const innerPromptsList = protocol._requestHandlers.get('prompts/list');
189
184
  if (innerPromptsList) {
190
- protocol.setRequestHandler(ListPromptsRequestSchema, wrapPromptsList(innerPromptsList, icon));
185
+ protocol.setRequestHandler('prompts/list', wrapPromptsList(innerPromptsList, icon));
191
186
  }
192
187
  }
@@ -2,12 +2,11 @@
2
2
  * stdio transport setup — extracted from server.js runServer().
3
3
  * Used when server is launched without the --http flag.
4
4
  */
5
-
6
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
7
6
 
8
7
  /**
9
8
  * Connect the MCP server to stdio transport and log startup message.
10
- * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
9
+ * @param {import('@modelcontextprotocol/server').McpServer} server
11
10
  */
12
11
  export async function connectStdio(server) {
13
12
  const transport = new StdioServerTransport();