crawlforge-mcp-server 3.0.18 → 3.4.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.
Files changed (50) hide show
  1. package/package.json +5 -2
  2. package/server.js +192 -1277
  3. package/src/core/ActionExecutor.js +2 -43
  4. package/src/core/AuthManager.js +127 -14
  5. package/src/core/BrowserContextPool.js +187 -0
  6. package/src/core/JobManager.js +7 -5
  7. package/src/core/LocalizationManager.js +14 -125
  8. package/src/core/StealthBrowserManager.js +26 -18
  9. package/src/core/cache/CacheManager.js +4 -1
  10. package/src/core/crawlers/BFSCrawler.js +19 -5
  11. package/src/observability/metrics.js +137 -0
  12. package/src/observability/tracing.js +74 -0
  13. package/src/server/auth/oauth.js +388 -0
  14. package/src/server/registerTool.js +41 -0
  15. package/src/server/schemas/common.js +29 -0
  16. package/src/server/transports/http.js +22 -0
  17. package/src/server/transports/stdio.js +16 -0
  18. package/src/server/transports/streamableHttp.js +226 -0
  19. package/src/server/withAuth.js +121 -0
  20. package/src/tools/advanced/BatchScrapeTool.js +12 -1086
  21. package/src/tools/advanced/ScrapeWithActionsTool.js +105 -19
  22. package/src/tools/advanced/batchScrape/index.js +328 -0
  23. package/src/tools/advanced/batchScrape/queue.js +91 -0
  24. package/src/tools/advanced/batchScrape/reporter.js +26 -0
  25. package/src/tools/advanced/batchScrape/schema.js +37 -0
  26. package/src/tools/advanced/batchScrape/worker.js +179 -0
  27. package/src/tools/advanced/scrapeWithActions/recorder.js +188 -0
  28. package/src/tools/basic/_fetch.js +35 -0
  29. package/src/tools/basic/extractLinks.js +74 -0
  30. package/src/tools/basic/extractMetadata.js +74 -0
  31. package/src/tools/basic/extractText.js +46 -0
  32. package/src/tools/basic/fetchUrl.js +44 -0
  33. package/src/tools/basic/scrapeStructured.js +58 -0
  34. package/src/tools/crawl/_sessionContext.js +234 -0
  35. package/src/tools/crawl/crawlDeep.js +55 -5
  36. package/src/tools/crawl/mapSite.js +23 -2
  37. package/src/tools/extract/_fetchAndParse.js +57 -0
  38. package/src/tools/extract/extractStructured.js +3 -19
  39. package/src/tools/extract/extractWithLlm.js +365 -0
  40. package/src/tools/search/providers/searxng.js +126 -0
  41. package/src/tools/search/ranking/ResultDeduplicator.js +18 -11
  42. package/src/tools/search/ranking/ResultRanker.js +17 -10
  43. package/src/tools/search/ranking/SearchResultCache.js +52 -0
  44. package/src/tools/search/searchWeb.js +112 -6
  45. package/src/tools/tracking/trackChanges/differ.js +98 -0
  46. package/src/tools/tracking/trackChanges/index.js +432 -0
  47. package/src/tools/tracking/trackChanges/monitor.js +93 -0
  48. package/src/tools/tracking/trackChanges/notifier.js +105 -0
  49. package/src/tools/tracking/trackChanges/schema.js +127 -0
  50. package/src/tools/tracking/trackChanges.js +12 -1374
@@ -0,0 +1,388 @@
1
+ /**
2
+ * OAuth 2.1 authorization for CrawlForge MCP Server.
3
+ *
4
+ * Implements the subset of OAuth 2.1 required by the MCP spec
5
+ * (modelcontextprotocol.io/docs/tutorials/security/authorization):
6
+ *
7
+ * - Authorization Server discovery: GET /.well-known/oauth-authorization-server
8
+ * - Dynamic Client Registration: POST /oauth/register
9
+ * - Authorization endpoint: GET /oauth/authorize (PKCE required)
10
+ * - Token endpoint: POST /oauth/token (PKCE code + refresh)
11
+ * - Token revocation: POST /oauth/revoke
12
+ *
13
+ * Token model: opaque bearer tokens minted server-side. Each access token
14
+ * carries a `mappedApiKey` link to a CrawlForge API key, so downstream
15
+ * credit-tracking continues to work unchanged.
16
+ *
17
+ * Storage: in-memory by default. Production deployments should provide
18
+ * `storage` adapter (set/get/delete) so tokens survive restarts.
19
+ *
20
+ * Stdio transport keeps the static API key — OAuth is HTTP-only.
21
+ *
22
+ * This module is intentionally dependency-free (no `oidc-provider`/`oauth4webapi`)
23
+ * to keep the npm install footprint small. It implements only what MCP requires.
24
+ */
25
+
26
+ import { randomBytes, createHash, timingSafeEqual } from 'node:crypto';
27
+
28
+ const ACCESS_TTL_MS = 60 * 60 * 1000; // 1 hour
29
+ const REFRESH_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
30
+ const CODE_TTL_MS = 5 * 60 * 1000; // 5 minutes
31
+ const DEFAULT_SCOPES = ['mcp:read', 'mcp:write'];
32
+
33
+ /**
34
+ * Create an OAuth provider bound to a CrawlForge API key.
35
+ *
36
+ * @param {object} options
37
+ * @param {string} options.issuer — public URL of this server, e.g. https://mcp.crawlforge.dev
38
+ * @param {string} options.apiKey — the CrawlForge API key to map tokens to
39
+ * @param {object} [options.storage] — { setClient, getClient, setCode, takeCode, setToken, getToken, deleteToken }
40
+ * @param {object} [options.logger] — Winston-style logger
41
+ * @param {string[]} [options.scopes] — supported scopes
42
+ * @param {number} [options.accessTtlMs]
43
+ * @param {number} [options.refreshTtlMs]
44
+ */
45
+ export function createOAuthProvider({
46
+ issuer,
47
+ apiKey,
48
+ storage,
49
+ logger = console,
50
+ scopes = DEFAULT_SCOPES,
51
+ accessTtlMs = ACCESS_TTL_MS,
52
+ refreshTtlMs = REFRESH_TTL_MS
53
+ }) {
54
+ if (!issuer) throw new Error('createOAuthProvider: issuer is required');
55
+ if (!apiKey) throw new Error('createOAuthProvider: apiKey is required');
56
+
57
+ const store = storage ?? createInMemoryStorage();
58
+
59
+ return {
60
+ issuer,
61
+ scopes,
62
+
63
+ /**
64
+ * Returns true if a request URL/method targets one of this provider's routes.
65
+ */
66
+ matches(url, method) {
67
+ if (!url) return false;
68
+ const path = url.split('?')[0];
69
+ if (path === '/.well-known/oauth-authorization-server' && method === 'GET') return true;
70
+ if (path === '/oauth/register' && method === 'POST') return true;
71
+ if (path === '/oauth/authorize' && method === 'GET') return true;
72
+ if (path === '/oauth/token' && method === 'POST') return true;
73
+ if (path === '/oauth/revoke' && method === 'POST') return true;
74
+ return false;
75
+ },
76
+
77
+ /**
78
+ * Validate an opaque bearer access token. Returns { ok, mappedApiKey } on success.
79
+ */
80
+ async validateBearer(token) {
81
+ if (!token) return { ok: false };
82
+ const record = await store.getToken(token);
83
+ if (!record) return { ok: false };
84
+ if (record.type !== 'access') return { ok: false };
85
+ if (record.expiresAt < Date.now()) {
86
+ await store.deleteToken(token);
87
+ return { ok: false };
88
+ }
89
+ return { ok: true, mappedApiKey: record.mappedApiKey, clientId: record.clientId, scopes: record.scopes };
90
+ },
91
+
92
+ /**
93
+ * Dispatch an OAuth request. Caller must have verified matches() first.
94
+ */
95
+ async handle(req, res) {
96
+ try {
97
+ const path = req.url.split('?')[0];
98
+ const method = req.method;
99
+
100
+ if (path === '/.well-known/oauth-authorization-server' && method === 'GET') {
101
+ return sendJson(res, 200, buildDiscovery(issuer, scopes));
102
+ }
103
+ if (path === '/oauth/register' && method === 'POST') {
104
+ return await handleRegister(req, res, store);
105
+ }
106
+ if (path === '/oauth/authorize' && method === 'GET') {
107
+ return await handleAuthorize(req, res, store, apiKey);
108
+ }
109
+ if (path === '/oauth/token' && method === 'POST') {
110
+ return await handleToken(req, res, store, apiKey, { accessTtlMs, refreshTtlMs });
111
+ }
112
+ if (path === '/oauth/revoke' && method === 'POST') {
113
+ return await handleRevoke(req, res, store);
114
+ }
115
+ return sendJson(res, 404, { error: 'not_found' });
116
+ } catch (err) {
117
+ logger.error?.('oauth handler error', { error: err?.message, stack: err?.stack });
118
+ return sendJson(res, 500, { error: 'server_error', error_description: err?.message ?? 'unknown' });
119
+ }
120
+ },
121
+
122
+ // Test helpers / internals
123
+ _store: store
124
+ };
125
+ }
126
+
127
+ // ─── Discovery ────────────────────────────────────────────────────────────────
128
+
129
+ function buildDiscovery(issuer, scopes) {
130
+ return {
131
+ issuer,
132
+ authorization_endpoint: `${issuer}/oauth/authorize`,
133
+ token_endpoint: `${issuer}/oauth/token`,
134
+ registration_endpoint: `${issuer}/oauth/register`,
135
+ revocation_endpoint: `${issuer}/oauth/revoke`,
136
+ response_types_supported: ['code'],
137
+ grant_types_supported: ['authorization_code', 'refresh_token'],
138
+ code_challenge_methods_supported: ['S256'],
139
+ token_endpoint_auth_methods_supported: ['none', 'client_secret_post'],
140
+ scopes_supported: scopes
141
+ };
142
+ }
143
+
144
+ // ─── Dynamic Client Registration (RFC 7591) ──────────────────────────────────
145
+
146
+ async function handleRegister(req, res, store) {
147
+ const body = await readJsonBody(req);
148
+ const redirectUris = Array.isArray(body?.redirect_uris) ? body.redirect_uris : [];
149
+ if (redirectUris.length === 0) {
150
+ return sendJson(res, 400, { error: 'invalid_redirect_uri', error_description: 'redirect_uris is required' });
151
+ }
152
+ for (const uri of redirectUris) {
153
+ if (typeof uri !== 'string' || !/^https?:\/\//.test(uri)) {
154
+ return sendJson(res, 400, { error: 'invalid_redirect_uri', error_description: `bad uri: ${uri}` });
155
+ }
156
+ }
157
+
158
+ const clientId = `cf-${randomBytes(8).toString('hex')}`;
159
+ // MCP spec recommends public clients (PKCE only). We don't issue secrets by default.
160
+ const client = {
161
+ client_id: clientId,
162
+ client_name: body?.client_name ?? 'mcp-client',
163
+ redirect_uris: redirectUris,
164
+ token_endpoint_auth_method: 'none',
165
+ grant_types: ['authorization_code', 'refresh_token'],
166
+ response_types: ['code'],
167
+ created_at: Date.now()
168
+ };
169
+ await store.setClient(clientId, client);
170
+
171
+ return sendJson(res, 201, client);
172
+ }
173
+
174
+ // ─── Authorization endpoint ──────────────────────────────────────────────────
175
+
176
+ async function handleAuthorize(req, res, store, apiKey) {
177
+ const url = new URL(req.url, 'http://x');
178
+ const params = Object.fromEntries(url.searchParams.entries());
179
+
180
+ const required = ['response_type', 'client_id', 'redirect_uri', 'code_challenge', 'code_challenge_method'];
181
+ for (const key of required) {
182
+ if (!params[key]) return sendJson(res, 400, { error: 'invalid_request', error_description: `missing ${key}` });
183
+ }
184
+ if (params.response_type !== 'code') {
185
+ return sendJson(res, 400, { error: 'unsupported_response_type' });
186
+ }
187
+ if (params.code_challenge_method !== 'S256') {
188
+ return sendJson(res, 400, { error: 'invalid_request', error_description: 'only S256 PKCE is supported' });
189
+ }
190
+
191
+ const client = await store.getClient(params.client_id);
192
+ if (!client) {
193
+ return sendJson(res, 400, { error: 'invalid_client' });
194
+ }
195
+ if (!client.redirect_uris.includes(params.redirect_uri)) {
196
+ return sendJson(res, 400, { error: 'invalid_redirect_uri' });
197
+ }
198
+
199
+ // Auto-approve: this server is a personal MCP endpoint backed by a single
200
+ // CrawlForge API key the operator has already authenticated. No consent UI
201
+ // is needed — possession of the operator's `apiKey` IS the authorization.
202
+ // (Same trust model as the static-key transport.)
203
+ //
204
+ // For a multi-tenant deployment, replace this with a real consent page that
205
+ // resolves to a CrawlForge user → API key mapping.
206
+ const code = randomBytes(24).toString('base64url');
207
+ await store.setCode(code, {
208
+ clientId: params.client_id,
209
+ redirectUri: params.redirect_uri,
210
+ codeChallenge: params.code_challenge,
211
+ scopes: (params.scope ?? 'mcp:read mcp:write').split(/\s+/).filter(Boolean),
212
+ mappedApiKey: apiKey,
213
+ expiresAt: Date.now() + CODE_TTL_MS
214
+ });
215
+
216
+ const redirect = new URL(params.redirect_uri);
217
+ redirect.searchParams.set('code', code);
218
+ if (params.state) redirect.searchParams.set('state', params.state);
219
+ res.writeHead(302, { Location: redirect.toString() });
220
+ res.end();
221
+ }
222
+
223
+ // ─── Token endpoint ──────────────────────────────────────────────────────────
224
+
225
+ async function handleToken(req, res, store, apiKey, { accessTtlMs, refreshTtlMs }) {
226
+ const body = await readFormBody(req);
227
+ const grant = body.grant_type;
228
+
229
+ if (grant === 'authorization_code') {
230
+ const required = ['code', 'redirect_uri', 'client_id', 'code_verifier'];
231
+ for (const key of required) {
232
+ if (!body[key]) return sendJson(res, 400, { error: 'invalid_request', error_description: `missing ${key}` });
233
+ }
234
+ const codeRecord = await store.takeCode(body.code);
235
+ if (!codeRecord) return sendJson(res, 400, { error: 'invalid_grant', error_description: 'unknown or used code' });
236
+ if (codeRecord.expiresAt < Date.now()) return sendJson(res, 400, { error: 'invalid_grant', error_description: 'code expired' });
237
+ if (codeRecord.clientId !== body.client_id) return sendJson(res, 400, { error: 'invalid_grant' });
238
+ if (codeRecord.redirectUri !== body.redirect_uri) return sendJson(res, 400, { error: 'invalid_grant' });
239
+ if (!verifyPkce(body.code_verifier, codeRecord.codeChallenge)) {
240
+ return sendJson(res, 400, { error: 'invalid_grant', error_description: 'PKCE verification failed' });
241
+ }
242
+
243
+ const tokens = await issueTokens(store, {
244
+ clientId: codeRecord.clientId,
245
+ mappedApiKey: codeRecord.mappedApiKey,
246
+ scopes: codeRecord.scopes,
247
+ accessTtlMs,
248
+ refreshTtlMs
249
+ });
250
+ return sendJson(res, 200, tokens);
251
+ }
252
+
253
+ if (grant === 'refresh_token') {
254
+ if (!body.refresh_token) return sendJson(res, 400, { error: 'invalid_request', error_description: 'missing refresh_token' });
255
+ const record = await store.getToken(body.refresh_token);
256
+ if (!record || record.type !== 'refresh') return sendJson(res, 400, { error: 'invalid_grant' });
257
+ if (record.expiresAt < Date.now()) {
258
+ await store.deleteToken(body.refresh_token);
259
+ return sendJson(res, 400, { error: 'invalid_grant', error_description: 'refresh expired' });
260
+ }
261
+ // Rotate refresh token (RFC 6749 §6, MCP best practice)
262
+ await store.deleteToken(body.refresh_token);
263
+ const tokens = await issueTokens(store, {
264
+ clientId: record.clientId,
265
+ mappedApiKey: record.mappedApiKey,
266
+ scopes: record.scopes,
267
+ accessTtlMs,
268
+ refreshTtlMs
269
+ });
270
+ return sendJson(res, 200, tokens);
271
+ }
272
+
273
+ return sendJson(res, 400, { error: 'unsupported_grant_type' });
274
+ }
275
+
276
+ // ─── Revocation (RFC 7009) ───────────────────────────────────────────────────
277
+
278
+ async function handleRevoke(req, res, store) {
279
+ const body = await readFormBody(req);
280
+ if (body.token) {
281
+ await store.deleteToken(body.token);
282
+ }
283
+ res.writeHead(200);
284
+ res.end();
285
+ }
286
+
287
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
288
+
289
+ async function issueTokens(store, { clientId, mappedApiKey, scopes, accessTtlMs, refreshTtlMs }) {
290
+ const accessToken = randomBytes(32).toString('base64url');
291
+ const refreshToken = randomBytes(32).toString('base64url');
292
+ const now = Date.now();
293
+ await store.setToken(accessToken, {
294
+ type: 'access',
295
+ clientId,
296
+ mappedApiKey,
297
+ scopes,
298
+ expiresAt: now + accessTtlMs
299
+ });
300
+ await store.setToken(refreshToken, {
301
+ type: 'refresh',
302
+ clientId,
303
+ mappedApiKey,
304
+ scopes,
305
+ expiresAt: now + refreshTtlMs
306
+ });
307
+ return {
308
+ access_token: accessToken,
309
+ token_type: 'Bearer',
310
+ expires_in: Math.floor(accessTtlMs / 1000),
311
+ refresh_token: refreshToken,
312
+ scope: scopes.join(' ')
313
+ };
314
+ }
315
+
316
+ function verifyPkce(verifier, expectedChallenge) {
317
+ try {
318
+ const challenge = createHash('sha256').update(verifier).digest('base64url');
319
+ const a = Buffer.from(challenge);
320
+ const b = Buffer.from(expectedChallenge);
321
+ if (a.length !== b.length) return false;
322
+ return timingSafeEqual(a, b);
323
+ } catch {
324
+ return false;
325
+ }
326
+ }
327
+
328
+ function sendJson(res, status, body) {
329
+ res.writeHead(status, { 'Content-Type': 'application/json' });
330
+ res.end(JSON.stringify(body));
331
+ }
332
+
333
+ async function readJsonBody(req) {
334
+ const raw = await readRawBody(req);
335
+ if (!raw) return {};
336
+ try {
337
+ return JSON.parse(raw);
338
+ } catch {
339
+ return {};
340
+ }
341
+ }
342
+
343
+ async function readFormBody(req) {
344
+ const raw = await readRawBody(req);
345
+ if (!raw) return {};
346
+ const ct = (req.headers['content-type'] || '').toLowerCase();
347
+ if (ct.includes('application/json')) {
348
+ try { return JSON.parse(raw); } catch { return {}; }
349
+ }
350
+ // application/x-www-form-urlencoded
351
+ const out = {};
352
+ for (const pair of raw.split('&')) {
353
+ if (!pair) continue;
354
+ const [k, v = ''] = pair.split('=');
355
+ out[decodeURIComponent(k)] = decodeURIComponent(v.replace(/\+/g, ' '));
356
+ }
357
+ return out;
358
+ }
359
+
360
+ function readRawBody(req) {
361
+ return new Promise((resolve, reject) => {
362
+ const chunks = [];
363
+ req.on('data', (chunk) => chunks.push(chunk));
364
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
365
+ req.on('error', reject);
366
+ });
367
+ }
368
+
369
+ // ─── In-memory storage (default) ─────────────────────────────────────────────
370
+
371
+ function createInMemoryStorage() {
372
+ const clients = new Map();
373
+ const codes = new Map();
374
+ const tokens = new Map();
375
+ return {
376
+ async setClient(id, client) { clients.set(id, client); },
377
+ async getClient(id) { return clients.get(id); },
378
+ async setCode(code, record) { codes.set(code, record); },
379
+ async takeCode(code) {
380
+ const r = codes.get(code);
381
+ codes.delete(code);
382
+ return r;
383
+ },
384
+ async setToken(token, record) { tokens.set(token, record); },
385
+ async getToken(token) { return tokens.get(token); },
386
+ async deleteToken(token) { tokens.delete(token); }
387
+ };
388
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * registerTool — thin wrapper around McpServer.registerTool that:
3
+ * 1. Accepts a plain descriptor object
4
+ * 2. Wraps the handler with withAuth (credit tracking + audit logging)
5
+ * 3. Optionally declares `outputSchema` (MCP SDK ≥1.10 structured outputs, C3)
6
+ *
7
+ * Structured outputs:
8
+ * When `outputSchema` is provided, the handler's return value should include
9
+ * `structuredContent` alongside the legacy `content` array. The MCP SDK
10
+ * validates `structuredContent` against the schema; legacy clients keep
11
+ * reading the JSON-stringified `content` for backward compatibility.
12
+ *
13
+ * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
14
+ * @param {Function} withAuth — from makeWithAuth() in src/server/withAuth.js
15
+ * @param {Object} descriptor
16
+ * @param {string} descriptor.name — tool name (MCP identifier)
17
+ * @param {string} descriptor.description — human-readable description
18
+ * @param {Object} descriptor.inputSchema — Zod shape (plain object, not z.object())
19
+ * @param {Object} [descriptor.outputSchema] — Zod shape for structured output (optional)
20
+ * @param {Object} [descriptor.annotations] — MCP annotations (readOnlyHint, etc.)
21
+ * @param {Function} descriptor.handler — async (params) => { content, structuredContent? }
22
+ */
23
+ export function registerTool(server, withAuth, { name, description, inputSchema, outputSchema, annotations = {}, handler }) {
24
+ const registration = { description, inputSchema, annotations };
25
+ if (outputSchema) registration.outputSchema = outputSchema;
26
+ server.registerTool(name, registration, withAuth(name, handler));
27
+ }
28
+
29
+ /**
30
+ * Helper for tool handlers that want to emit both legacy `content` and
31
+ * MCP-2025-06-18 `structuredContent` from one shot.
32
+ *
33
+ * @param {object} structured — JSON-serializable object matching outputSchema
34
+ * @returns {{content: object[], structuredContent: object}}
35
+ */
36
+ export function dualOutput(structured) {
37
+ return {
38
+ structuredContent: structured,
39
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }]
40
+ };
41
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared Zod schema fragments reused across tool registrations in server.js.
3
+ * Centralizes repeated patterns (url, pagination, webhook, cache opts).
4
+ */
5
+
6
+ import { z } from 'zod';
7
+
8
+ /** URL field — string that validates as a URL */
9
+ export const urlSchema = z.string().url();
10
+
11
+ /** Optional pagination fields shared by history / list endpoints */
12
+ export const paginationSchema = z.object({
13
+ limit: z.number().min(1).max(500).default(50),
14
+ offset: z.number().min(0).default(0)
15
+ });
16
+
17
+ /** Webhook notification object, used in batch_scrape and deep_research */
18
+ export const webhookSchema = z.object({
19
+ url: urlSchema,
20
+ events: z.array(z.string()).optional(),
21
+ headers: z.record(z.string()).optional(),
22
+ signingSecret: z.string().optional()
23
+ });
24
+
25
+ /** Cache behaviour flags, used in crawl and search tools */
26
+ export const cacheOptsSchema = z.object({
27
+ enabled: z.boolean().default(true),
28
+ ttl: z.number().min(0).default(3600000)
29
+ });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * HTTP transport — back-compat shim.
3
+ *
4
+ * As of v3.2.0 ("Modernize") the canonical HTTP entry point is
5
+ * `connectStreamableHttp` in ./streamableHttp.js. This module is retained
6
+ * so older imports (`./http.js`) keep working; it forwards to the new
7
+ * implementation in stateless ("legacy") mode by default.
8
+ *
9
+ * @deprecated Use connectStreamableHttp from ./streamableHttp.js
10
+ */
11
+
12
+ import { connectStreamableHttp } from './streamableHttp.js';
13
+
14
+ /**
15
+ * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
16
+ * @param {import('../../core/AuthManager.js').default} authManager
17
+ * @param {import('../../utils/Logger.js').logger} logger
18
+ * @param {number} [port=3000]
19
+ */
20
+ export async function connectHttp(server, authManager, logger, port = 3000) {
21
+ return connectStreamableHttp(server, authManager, logger, { port, legacy: true });
22
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * stdio transport setup — extracted from server.js runServer().
3
+ * Used when server is launched without the --http flag.
4
+ */
5
+
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
+
8
+ /**
9
+ * Connect the MCP server to stdio transport and log startup message.
10
+ * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
11
+ */
12
+ export async function connectStdio(server) {
13
+ const transport = new StdioServerTransport();
14
+ await server.connect(transport);
15
+ console.error('CrawlForge MCP Server v3.0 running on stdio');
16
+ }