@opencraw/mcp 0.1.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 ADDED
@@ -0,0 +1,103 @@
1
+ # @opencraw/mcp
2
+
3
+ An MCP (Model Context Protocol) server exposing [`@opencraw/core`](../core) as tools an agent can call
4
+ directly: probe a page, validate recipes, run a crawl, list what's already authored. Local transport only
5
+ (stdio) — the host launches it as a subprocess, the same shape as the [`opencraw` cli](../cli).
6
+
7
+ This does **not** auto-author recipes from a sentence. The four tools are the same primitives the
8
+ `opencraw` cli gives a terminal; the calling agent still writes the JSON recipes, using `probe` and
9
+ `validate` to iterate, the same way this project's own example recipes were built by hand. An agent that
10
+ can write files passes their `paths`; one that can't (a chat-only host) passes the `recipes` inline.
11
+
12
+ ## Register it
13
+
14
+ ```json
15
+ {
16
+ "mcpServers": {
17
+ "opencraw": {
18
+ "command": "node",
19
+ "args": ["/absolute/path/to/opencraw/packages/mcp/bin/opencraw-mcp.mjs"],
20
+ "env": { "OPENCRAW_CHROMIUM": "/path/to/chrome" }
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ `OPENCRAW_CHROMIUM` and `OPENCRAW_INSECURE_TLS=1` in the server's environment are the defaults for every
27
+ `probe`/`run` call's `browserPath`/`insecureTls`, so a sandbox without Playwright's own bundled browser
28
+ does not need every tool call to repeat them.
29
+
30
+ `OPENCRAW_HOOKS` points at a JavaScript module whose default export is `{ name: function }`: the hooks the
31
+ recipes call (`hook` steps and transforms). Only the server's environment names it, never a tool call, so an
32
+ agent can run recipes that use your hooks but can't make the server load a module of its choosing.
33
+
34
+ `OPENCRAW_ACCESS` points at an access config ([access.md](../../docs/recipes/access.md)): proxy profiles, with
35
+ credentials as `{{env.NAME}}` read from the server's environment. The `probe` and `run` tools then take an
36
+ `access` argument naming a profile. The file and the credentials never pass through a tool call.
37
+
38
+ ## Tools
39
+
40
+ ### `probe`
41
+
42
+ Fetches a page and reports where its data lives: JSON-LD blocks, inline JSON objects above a size with
43
+ their keys, `.json` URLs referenced in the markup, script hosts, and links that look like an API. With
44
+ `browser: true` it also renders the page and lists the JSON responses observed while it settles, for
45
+ endpoints only a script fetches after load.
46
+
47
+ | Input | Meaning |
48
+ |---|---|
49
+ | `url` | The page to fetch. |
50
+ | `browser?` | Also render it in a browser (slower, a few seconds). |
51
+ | `browserPath?`, `insecureTls?`, `userAgent?` | As in the cli. |
52
+ | `access?` | An access profile from the server's `OPENCRAW_ACCESS` config. |
53
+
54
+ Returns `{ url, status, findings: { jsonLd, inlineJson, jsonUrls, scriptHosts, apiLinks }, observed }`.
55
+
56
+ ### `validate`
57
+
58
+ Loads and binds recipes (an output recipe plus its input recipes), from files or passed inline: parses each
59
+ against its schema, checks every mapping resolves, reports every problem with its JSON path.
60
+
61
+ | Input | Meaning |
62
+ |---|---|
63
+ | `paths` | Recipe files (`.json`, `.jsonl`) or directories of them. |
64
+ | `recipes` | Instead of `paths`: the recipes themselves, as an array of recipe objects or as JSON / JSON Lines text. For a host that can't write files. An inline recipe's issues name it by position: `recipes[1]`, or `recipes:2` for a line. |
65
+
66
+ Give exactly one of `paths` and `recipes`.
67
+
68
+ Returns `{ ok, output?, inputs: string[], issues: [{ path, message, source }] }`.
69
+
70
+ ### `run`
71
+
72
+ Crawls the recipes at the given paths, or passed inline.
73
+
74
+ | Input | Meaning |
75
+ |---|---|
76
+ | `paths` or `recipes` | As in `validate`: exactly one output recipe, any number of input recipes. |
77
+ | `out?` | Write records to this JSON Lines file instead of returning them inline. Use this for anything beyond a handful of records — the tool result is not the place for a large crawl's output. |
78
+ | `append?`, `resume?` | As in the cli (`resume` needs `append`). |
79
+ | `only?` | Run only these input recipe ids. |
80
+ | `dryRun?` | One record per input recipe instead of a full crawl — for checking a recipe under construction. |
81
+ | `headed?`, `browserPath?`, `insecureTls?`, `userAgent?` | As in the cli. |
82
+ | `access?` | The access profile for recipes that name none, from the server's `OPENCRAW_ACCESS` config. |
83
+
84
+ Returns `{ report: CrawlReport, records?, truncated? }`. `records`/`truncated` are present only when `out`
85
+ was not given, and `records` is capped at 50 even then.
86
+
87
+ ### `list_recipes`
88
+
89
+ Lists the recipe files in a directory, split by kind, with their ids — cheaper than a full `validate`
90
+ call, for checking what already exists before writing a new recipe.
91
+
92
+ | Input | Meaning |
93
+ |---|---|
94
+ | `dir` | A directory of recipe files. |
95
+
96
+ Returns `{ outputs: [{ path, id }], inputs: [{ path, id, output, mode }], others: string[] }`.
97
+
98
+ ## Building
99
+
100
+ `nx build @opencraw/mcp`, `nx test @opencraw/mcp` (unit tests per tool), `nx run @opencraw/mcp:e2e`
101
+ (spawns the built server and drives it with the MCP SDK's own `Client`/`StdioClientTransport`, against the
102
+ fixture shop `@opencraw/core` ships; needs a browser — `npm run playwright:install` once, or
103
+ `OPENCRAW_CHROMIUM=/path/to/chrome`).
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../dist/index.esm.js'
3
+
4
+ await main()
@@ -0,0 +1 @@
1
+ export * from "./src/index.js";
@@ -0,0 +1,373 @@
1
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { readRecipeFiles, probeUrl, loadForRun, resolveAccess, loadHooks } from '@opencraw/cli';
4
+ import { z } from 'zod';
5
+ import { memorySink, jsonLinesSink, createCrawler, RecipeSet, RecipeValidationError, RecipeBindingError, parseOutputRecipe, parseInputRecipe, bindRecipeSet } from '@opencraw/core';
6
+
7
+ /** Input schema for the `list_recipes` tool. */
8
+ const listRecipesInputShape = {
9
+ dir: z.string().describe('A directory of recipe files.')
10
+ };
11
+ /**
12
+ * The `list_recipes` tool: what recipes already exist in a directory, cheaper
13
+ * than a full `validate` call, so an agent can check before writing a new one
14
+ * or find what an existing output recipe already covers.
15
+ *
16
+ * @param args - The tool's parsed input.
17
+ * @returns The MCP tool result.
18
+ */
19
+ async function listRecipesTool(args) {
20
+ const files = await readRecipeFiles([args.dir]);
21
+ const result = {
22
+ outputs: files.outputs.map(file => ({
23
+ path: file.path,
24
+ id: idOf(file.recipe)
25
+ })),
26
+ inputs: files.inputs.map(file => ({
27
+ path: file.path,
28
+ id: idOf(file.recipe),
29
+ output: fieldOf(file.recipe, 'output'),
30
+ mode: fieldOf(file.recipe, 'mode')
31
+ })),
32
+ others: files.others
33
+ };
34
+ return {
35
+ content: [{
36
+ type: 'text',
37
+ text: JSON.stringify(result)
38
+ }],
39
+ structuredContent: result
40
+ };
41
+ }
42
+ function idOf(recipe) {
43
+ return fieldOf(recipe, 'id');
44
+ }
45
+ function fieldOf(recipe, name) {
46
+ if (typeof recipe !== 'object' || recipe === null) return undefined;
47
+ const value = recipe[name];
48
+ return typeof value === 'string' ? value : undefined;
49
+ }
50
+
51
+ /** Input schema for the `probe` tool: a raw zod shape, as `McpServer.registerTool` expects. */
52
+ const probeInputShape = {
53
+ url: z.string().describe('The page to fetch and inspect.'),
54
+ browser: z.boolean().optional().describe('Also render the page in a browser and list the JSON responses it fetches while settling. Slower (a few seconds); finds endpoints a plain fetch of the initial HTML cannot.'),
55
+ browserPath: z.string().optional().describe('A browser binary other than the one Playwright installed. Defaults to OPENCRAW_CHROMIUM in the server\'s environment.'),
56
+ insecureTls: z.boolean().optional().describe("Accept an intercepting proxy's certificate. Defaults to OPENCRAW_INSECURE_TLS=1 in the server's environment."),
57
+ userAgent: z.string().optional().describe('The user agent to send.'),
58
+ access: z.string().optional().describe('An access profile (a proxy) from the server\'s access config file, OPENCRAW_ACCESS.')
59
+ };
60
+ /**
61
+ * The `probe` tool: fetches a page and reports where its data lives, so an
62
+ * agent can write an extraction recipe without guessing selectors.
63
+ *
64
+ * @param args - The tool's parsed input.
65
+ * @returns The MCP tool result: the probe findings as JSON text.
66
+ */
67
+ async function probeTool(args) {
68
+ try {
69
+ const result = await probeUrl(args.url, {
70
+ browser: args.browser ?? false,
71
+ browserPath: args.browserPath ?? process.env.OPENCRAW_CHROMIUM,
72
+ insecureTls: args.insecureTls === true || process.env.OPENCRAW_INSECURE_TLS === '1',
73
+ userAgent: args.userAgent,
74
+ access: process.env.OPENCRAW_ACCESS === '' ? undefined : process.env.OPENCRAW_ACCESS,
75
+ accessProfile: args.access
76
+ });
77
+ return {
78
+ content: [{
79
+ type: 'text',
80
+ text: JSON.stringify(result)
81
+ }],
82
+ structuredContent: result
83
+ };
84
+ } catch (error) {
85
+ return {
86
+ content: [{
87
+ type: 'text',
88
+ text: `probe failed: ${error instanceof Error ? error.message : String(error)}`
89
+ }],
90
+ isError: true
91
+ };
92
+ }
93
+ }
94
+
95
+ const recipeObject = z.record(z.string(), z.unknown());
96
+ const recipeObjects = z.array(recipeObject).min(1);
97
+ /** The two ways a tool takes recipes: files, or the recipes themselves. */
98
+ const recipeSourceShape = {
99
+ paths: z.array(z.string()).min(1).optional().describe('Recipe files or directories (every .json and .jsonl file in a directory is read). Give this or "recipes".'),
100
+ recipes: z.union([z.string(), recipeObjects]).optional().describe('The recipes themselves instead of files: an array of recipe objects, or JSON / JSON Lines text (one recipe per line). For a host that cannot write files. Give this or "paths".')
101
+ };
102
+ /**
103
+ * The recipe source a tool call names.
104
+ *
105
+ * @param args - The tool's parsed input.
106
+ * @returns What core reads: the paths, or the inline recipes.
107
+ * @throws Error when the call gives both or neither.
108
+ */
109
+ function recipeSourceOf(args) {
110
+ if (args.paths === undefined === (args.recipes === undefined)) throw new Error('give exactly one of "paths" or "recipes"');
111
+ if (args.paths !== undefined) return args.paths;
112
+ // Inline text is always recipes, never a path, whatever it starts with.
113
+ return typeof args.recipes === 'string' ? Buffer.from(args.recipes) : args.recipes ?? [];
114
+ }
115
+
116
+ const RECORD_CAP = 50;
117
+ /** Input schema for the `run` tool. */
118
+ const runInputShape = {
119
+ ...recipeSourceShape,
120
+ out: z.string().optional().describe('Write records to this JSON Lines file instead of returning them inline. Use for a crawl expected to produce more than a handful of records.'),
121
+ append: z.boolean().optional().describe('Keep what "out" holds and add to it; each line carries _key. Needs "out".'),
122
+ resume: z.boolean().optional().describe('Skip records "out" already has. Needs "append".'),
123
+ only: z.array(z.string()).optional().describe('Run only these input recipe ids.'),
124
+ dryRun: z.boolean().optional().describe('One record per input recipe, with the scope it was mapped from, instead of a full crawl. For checking a recipe under construction.'),
125
+ headed: z.boolean().optional().describe('Show the browser instead of running headless.'),
126
+ browserPath: z.string().optional().describe('A browser binary other than the one Playwright installed. Defaults to OPENCRAW_CHROMIUM in the server\'s environment.'),
127
+ insecureTls: z.boolean().optional().describe("Accept an intercepting proxy's certificate. Defaults to OPENCRAW_INSECURE_TLS=1 in the server's environment."),
128
+ userAgent: z.string().optional().describe('The user agent to send.'),
129
+ access: z.string().optional().describe('An access profile (a proxy) from the server\'s access config file, OPENCRAW_ACCESS, used by every recipe that names none. The error for an unknown name lists the profiles.')
130
+ };
131
+ /**
132
+ * The `run` tool: crawls the recipes in `paths` or `recipes` (exactly one
133
+ * output recipe, any number of inputs) and returns what happened,
134
+ * structured, instead of the cli's printed summary.
135
+ *
136
+ * @param args - The tool's parsed input.
137
+ * @returns The MCP tool result.
138
+ */
139
+ async function runTool(args) {
140
+ let set;
141
+ try {
142
+ set = await loadForRun(recipeSourceOf(args), args.only ?? []);
143
+ } catch (error) {
144
+ return {
145
+ content: [{
146
+ type: 'text',
147
+ text: loadErrorText(error)
148
+ }],
149
+ isError: true
150
+ };
151
+ }
152
+ if (set.inputs.length === 0) {
153
+ return {
154
+ content: [{
155
+ type: 'text',
156
+ text: args.only !== undefined && args.only.length > 0 ? `no input recipe matches "only": ${args.only.join(', ')}` : 'no input recipes found'
157
+ }],
158
+ isError: true
159
+ };
160
+ }
161
+ let crawler;
162
+ const sink = args.dryRun === true || args.out === undefined ? memorySink() : jsonLinesSink(args.out, {
163
+ append: args.append
164
+ });
165
+ try {
166
+ const hooksFile = serverFile('OPENCRAW_HOOKS');
167
+ crawler = createCrawler({
168
+ sink,
169
+ hooks: hooksFile === undefined ? undefined : await loadHooks(hooksFile),
170
+ access: await resolveAccess({
171
+ insecureTls: false,
172
+ access: serverFile('OPENCRAW_ACCESS'),
173
+ accessProfile: args.access
174
+ }),
175
+ resume: args.resume,
176
+ browser: {
177
+ headless: args.headed !== true,
178
+ executablePath: args.browserPath ?? process.env.OPENCRAW_CHROMIUM,
179
+ ignoreHTTPSErrors: args.insecureTls === true || process.env.OPENCRAW_INSECURE_TLS === '1'
180
+ }
181
+ });
182
+ } catch (error) {
183
+ return {
184
+ content: [{
185
+ type: 'text',
186
+ text: error instanceof Error ? error.message : String(error)
187
+ }],
188
+ isError: true
189
+ };
190
+ }
191
+ try {
192
+ const inputs = args.dryRun === true ? set.inputs.map(input => ({
193
+ ...input,
194
+ limits: {
195
+ ...input.limits,
196
+ maxRecords: 1
197
+ }
198
+ })) : set.inputs;
199
+ const report = await crawler.run(new RecipeSet(set.output, inputs));
200
+ const result = {
201
+ report
202
+ };
203
+ if (args.out === undefined) {
204
+ const records = sink.records.map(record => record.data);
205
+ result.records = records.slice(0, RECORD_CAP);
206
+ result.truncated = records.length > RECORD_CAP;
207
+ }
208
+ return {
209
+ content: [{
210
+ type: 'text',
211
+ text: JSON.stringify(result)
212
+ }],
213
+ structuredContent: result,
214
+ isError: report.recipes.some(recipe => recipe.error !== undefined)
215
+ };
216
+ } finally {
217
+ await crawler.close();
218
+ }
219
+ }
220
+ /**
221
+ * A file the server was started with (`OPENCRAW_ACCESS`, `OPENCRAW_HOOKS`).
222
+ * Never a tool argument: a caller picks a profile, never a file, and never
223
+ * names a module for the server to run.
224
+ */
225
+ function serverFile(name) {
226
+ const file = process.env[name];
227
+ return file === undefined || file === '' ? undefined : file;
228
+ }
229
+ function loadErrorText(error) {
230
+ if (error instanceof RecipeValidationError) return error.issues.map(issue => `${error.source}: ${issue.path === '' ? '(root)' : issue.path}: ${issue.message}`).join('\n');
231
+ if (error instanceof RecipeBindingError) return error.issues.map(issue => `${issue.recipeId}: ${issue.path}: ${issue.message}`).join('\n');
232
+ return error instanceof Error ? error.message : String(error);
233
+ }
234
+
235
+ /** Input schema for the `validate` tool. */
236
+ const validateInputShape = recipeSourceShape;
237
+ /**
238
+ * The `validate` tool: loads and binds recipes, returning every problem
239
+ * with its JSON path instead of a printed report, so an agent authoring a
240
+ * recipe can check it without a round trip through a terminal.
241
+ *
242
+ * @param args - The tool's parsed input.
243
+ * @returns The MCP tool result.
244
+ */
245
+ async function validateTool(args) {
246
+ let files;
247
+ try {
248
+ files = await readRecipeFiles(recipeSourceOf(args));
249
+ } catch (error) {
250
+ return reply({
251
+ ok: false,
252
+ inputs: [],
253
+ issues: issuesOf(error, '')
254
+ });
255
+ }
256
+ const issues = files.others.map(path => ({
257
+ path: '',
258
+ message: '"kind" is missing or not "input"/"output"',
259
+ source: path
260
+ }));
261
+ if (files.outputs.length === 0) issues.push({
262
+ path: '',
263
+ message: 'no output recipe found',
264
+ source: ''
265
+ });
266
+ if (files.outputs.length > 1) issues.push({
267
+ path: '',
268
+ message: `more than one output recipe: ${files.outputs.map(file => file.path).join(', ')}`,
269
+ source: ''
270
+ });
271
+ const result = {
272
+ ok: issues.length === 0,
273
+ inputs: [],
274
+ issues
275
+ };
276
+ if (files.outputs.length === 1) {
277
+ try {
278
+ const output = parseOutputRecipe(files.outputs[0].recipe, files.outputs[0].path);
279
+ result.output = output.id;
280
+ const inputs = files.inputs.flatMap(file => {
281
+ try {
282
+ const input = parseInputRecipe(file.recipe, file.path);
283
+ result.inputs.push(input.id);
284
+ return [input];
285
+ } catch (error) {
286
+ issues.push(...issuesOf(error, file.path));
287
+ return [];
288
+ }
289
+ });
290
+ try {
291
+ bindRecipeSet(output, inputs);
292
+ } catch (error) {
293
+ issues.push(...issuesOf(error, output.id));
294
+ }
295
+ } catch (error) {
296
+ issues.push(...issuesOf(error, files.outputs[0].path));
297
+ }
298
+ }
299
+ result.ok = issues.length === 0;
300
+ return reply(result);
301
+ }
302
+ function reply(result) {
303
+ return {
304
+ content: [{
305
+ type: 'text',
306
+ text: JSON.stringify(result)
307
+ }],
308
+ structuredContent: result
309
+ };
310
+ }
311
+ function issuesOf(error, source) {
312
+ if (error instanceof RecipeValidationError) return error.issues.map(issue => ({
313
+ path: issue.path,
314
+ message: issue.message,
315
+ source: error.source
316
+ }));
317
+ if (error instanceof RecipeBindingError) return error.issues.map(issue => ({
318
+ path: issue.path,
319
+ message: issue.message,
320
+ source: issue.recipeId
321
+ }));
322
+ return [{
323
+ path: '',
324
+ message: error instanceof Error ? error.message : String(error),
325
+ source
326
+ }];
327
+ }
328
+
329
+ const SERVER_INFO = {
330
+ name: 'opencraw',
331
+ version: '0.0.1'
332
+ };
333
+ /**
334
+ * Builds the MCP server: one tool per crawl primitive (`probe`, `validate`,
335
+ * `run`, `list_recipes`), each a thin wrapper over `@opencraw/core` and
336
+ * `@opencraw/cli`'s structured helpers. No transport is attached; `main`
337
+ * connects it over stdio.
338
+ *
339
+ * @returns The server, ready to `connect`.
340
+ */
341
+ function createServer() {
342
+ const server = new McpServer(SERVER_INFO);
343
+ server.registerTool('probe', {
344
+ description: 'Fetch a page and report where its data lives: JSON-LD blocks, inline JSON objects, .json URLs, script hosts, API-looking links. Use before writing an input recipe, to see the shape a site\'s data actually takes.',
345
+ inputSchema: probeInputShape
346
+ }, args => probeTool(args));
347
+ server.registerTool('validate', {
348
+ description: 'Load and bind recipes (an output recipe plus its input recipes), from files or passed inline: parse each against its schema, check every mapping resolves, report every problem with its JSON path.',
349
+ inputSchema: validateInputShape
350
+ }, args => validateTool(args));
351
+ server.registerTool('run', {
352
+ description: 'Crawl the recipes at the given paths, or passed inline as "recipes". Use dryRun to check a recipe under construction (one record per input, returned inline); use "out" for anything beyond a handful of records.',
353
+ inputSchema: runInputShape
354
+ }, args => runTool(args));
355
+ server.registerTool('list_recipes', {
356
+ description: 'List the recipe files in a directory, split by kind, with their ids. Check before writing a new recipe, or to see what an existing output recipe covers.',
357
+ inputSchema: listRecipesInputShape
358
+ }, args => listRecipesTool(args));
359
+ return server;
360
+ }
361
+
362
+ /**
363
+ * Runs the server over stdio until the transport closes. This is what
364
+ * `bin/opencraw-mcp.mjs` calls; an MCP host launches it as a subprocess and
365
+ * talks JSON-RPC over stdin/stdout, so there is no argv to parse.
366
+ */
367
+ async function main() {
368
+ const server = createServer();
369
+ await server.connect(new StdioServerTransport());
370
+ }
371
+
372
+ export { createServer, listRecipesInputShape, listRecipesTool, main, probeInputShape, probeTool, runInputShape, runTool, validateInputShape, validateTool };
373
+ //# sourceMappingURL=index.esm.js.map
@@ -0,0 +1,10 @@
1
+ export { main } from './main.js';
2
+ export { createServer } from './server/index.js';
3
+ export { probeTool, probeInputShape } from './probe-tool/index.js';
4
+ export { validateTool, validateInputShape } from './validate-tool/index.js';
5
+ export type { ValidateResult } from './validate-tool/index.js';
6
+ export { runTool, runInputShape } from './run-tool/index.js';
7
+ export type { RunResult } from './run-tool/index.js';
8
+ export { listRecipesTool, listRecipesInputShape } from './list-tool/index.js';
9
+ export type { ListRecipesResult } from './list-tool/index.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export { listRecipesTool, listRecipesInputShape } from './list-recipes-tool.handler.js';
2
+ export type { ListRecipesResult } from './list-recipes-tool.handler.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,32 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ import { z } from 'zod';
3
+ /** Input schema for the `list_recipes` tool. */
4
+ export declare const listRecipesInputShape: {
5
+ dir: z.ZodString;
6
+ };
7
+ /** What the `list_recipes` tool returns. */
8
+ export interface ListRecipesResult {
9
+ outputs: {
10
+ path: string;
11
+ id?: string;
12
+ }[];
13
+ inputs: {
14
+ path: string;
15
+ id?: string;
16
+ output?: string;
17
+ mode?: string;
18
+ }[];
19
+ others: string[];
20
+ }
21
+ /**
22
+ * The `list_recipes` tool: what recipes already exist in a directory, cheaper
23
+ * than a full `validate` call, so an agent can check before writing a new one
24
+ * or find what an existing output recipe already covers.
25
+ *
26
+ * @param args - The tool's parsed input.
27
+ * @returns The MCP tool result.
28
+ */
29
+ export declare function listRecipesTool(args: {
30
+ dir: string;
31
+ }): Promise<CallToolResult>;
32
+ //# sourceMappingURL=list-recipes-tool.handler.d.ts.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Runs the server over stdio until the transport closes. This is what
3
+ * `bin/opencraw-mcp.mjs` calls; an MCP host launches it as a subprocess and
4
+ * talks JSON-RPC over stdin/stdout, so there is no argv to parse.
5
+ */
6
+ export declare function main(): Promise<void>;
7
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1,2 @@
1
+ export { probeTool, probeInputShape } from './probe-tool.handler.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,27 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ import { z } from 'zod';
3
+ /** Input schema for the `probe` tool: a raw zod shape, as `McpServer.registerTool` expects. */
4
+ export declare const probeInputShape: {
5
+ url: z.ZodString;
6
+ browser: z.ZodOptional<z.ZodBoolean>;
7
+ browserPath: z.ZodOptional<z.ZodString>;
8
+ insecureTls: z.ZodOptional<z.ZodBoolean>;
9
+ userAgent: z.ZodOptional<z.ZodString>;
10
+ access: z.ZodOptional<z.ZodString>;
11
+ };
12
+ /**
13
+ * The `probe` tool: fetches a page and reports where its data lives, so an
14
+ * agent can write an extraction recipe without guessing selectors.
15
+ *
16
+ * @param args - The tool's parsed input.
17
+ * @returns The MCP tool result: the probe findings as JSON text.
18
+ */
19
+ export declare function probeTool(args: {
20
+ url: string;
21
+ browser?: boolean;
22
+ browserPath?: string;
23
+ insecureTls?: boolean;
24
+ userAgent?: string;
25
+ access?: string;
26
+ }): Promise<CallToolResult>;
27
+ //# sourceMappingURL=probe-tool.handler.d.ts.map
@@ -0,0 +1,3 @@
1
+ export { recipeSourceShape, recipeSourceOf } from './recipe-source.mapper.js';
2
+ export type { RecipeSourceArgs } from './recipe-source.mapper.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,21 @@
1
+ import type { RecipeSource } from '@opencraw/core';
2
+ import { z } from 'zod';
3
+ /** The two ways a tool takes recipes: files, or the recipes themselves. */
4
+ export declare const recipeSourceShape: {
5
+ paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
6
+ recipes: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>]>>;
7
+ };
8
+ /** A tool's recipe arguments. */
9
+ export interface RecipeSourceArgs {
10
+ paths?: string[];
11
+ recipes?: string | Record<string, unknown>[];
12
+ }
13
+ /**
14
+ * The recipe source a tool call names.
15
+ *
16
+ * @param args - The tool's parsed input.
17
+ * @returns What core reads: the paths, or the inline recipes.
18
+ * @throws Error when the call gives both or neither.
19
+ */
20
+ export declare function recipeSourceOf(args: RecipeSourceArgs): RecipeSource;
21
+ //# sourceMappingURL=recipe-source.mapper.d.ts.map
@@ -0,0 +1,3 @@
1
+ export { runTool, runInputShape } from './run-tool.handler.js';
2
+ export type { RunResult } from './run-tool.handler.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,49 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ import type { CrawlReport } from '@opencraw/core';
3
+ import { z } from 'zod';
4
+ import type { RecipeSourceArgs } from '../recipe-source/index.js';
5
+ /** Input schema for the `run` tool. */
6
+ export declare const runInputShape: {
7
+ out: z.ZodOptional<z.ZodString>;
8
+ append: z.ZodOptional<z.ZodBoolean>;
9
+ resume: z.ZodOptional<z.ZodBoolean>;
10
+ only: z.ZodOptional<z.ZodArray<z.ZodString>>;
11
+ dryRun: z.ZodOptional<z.ZodBoolean>;
12
+ headed: z.ZodOptional<z.ZodBoolean>;
13
+ browserPath: z.ZodOptional<z.ZodString>;
14
+ insecureTls: z.ZodOptional<z.ZodBoolean>;
15
+ userAgent: z.ZodOptional<z.ZodString>;
16
+ access: z.ZodOptional<z.ZodString>;
17
+ paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
18
+ recipes: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>]>>;
19
+ };
20
+ interface RunArgs extends RecipeSourceArgs {
21
+ out?: string;
22
+ append?: boolean;
23
+ resume?: boolean;
24
+ only?: string[];
25
+ dryRun?: boolean;
26
+ headed?: boolean;
27
+ browserPath?: string;
28
+ insecureTls?: boolean;
29
+ userAgent?: string;
30
+ access?: string;
31
+ }
32
+ /** What the `run` tool returns. */
33
+ export interface RunResult {
34
+ report: CrawlReport;
35
+ /** Present when "out" was not given: the records this call produced, capped. */
36
+ records?: Record<string, unknown>[];
37
+ truncated?: boolean;
38
+ }
39
+ /**
40
+ * The `run` tool: crawls the recipes in `paths` or `recipes` (exactly one
41
+ * output recipe, any number of inputs) and returns what happened,
42
+ * structured, instead of the cli's printed summary.
43
+ *
44
+ * @param args - The tool's parsed input.
45
+ * @returns The MCP tool result.
46
+ */
47
+ export declare function runTool(args: RunArgs): Promise<CallToolResult>;
48
+ export {};
49
+ //# sourceMappingURL=run-tool.handler.d.ts.map
@@ -0,0 +1,11 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ /**
3
+ * Builds the MCP server: one tool per crawl primitive (`probe`, `validate`,
4
+ * `run`, `list_recipes`), each a thin wrapper over `@opencraw/core` and
5
+ * `@opencraw/cli`'s structured helpers. No transport is attached; `main`
6
+ * connects it over stdio.
7
+ *
8
+ * @returns The server, ready to `connect`.
9
+ */
10
+ export declare function createServer(): McpServer;
11
+ //# sourceMappingURL=create-server.use-case.d.ts.map
@@ -0,0 +1,2 @@
1
+ export { createServer } from './create-server.use-case.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export { validateTool, validateInputShape } from './validate-tool.handler.js';
2
+ export type { ValidateResult } from './validate-tool.handler.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,31 @@
1
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ import type { RecipeSourceArgs } from '../recipe-source/index.js';
3
+ /** Input schema for the `validate` tool. */
4
+ export declare const validateInputShape: {
5
+ paths: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
6
+ recipes: import("zod").ZodOptional<import("zod").ZodUnion<readonly [import("zod").ZodString, import("zod").ZodArray<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>]>>;
7
+ };
8
+ interface Issue {
9
+ path: string;
10
+ message: string;
11
+ /** The file (`path:line` in JSON Lines), `recipes[i]` / `recipes:line` for inline recipes, or the recipe id when the issue came from binding. */
12
+ source: string;
13
+ }
14
+ /** What the `validate` tool returns. */
15
+ export interface ValidateResult {
16
+ ok: boolean;
17
+ output?: string;
18
+ inputs: string[];
19
+ issues: Issue[];
20
+ }
21
+ /**
22
+ * The `validate` tool: loads and binds recipes, returning every problem
23
+ * with its JSON path instead of a printed report, so an agent authoring a
24
+ * recipe can check it without a round trip through a terminal.
25
+ *
26
+ * @param args - The tool's parsed input.
27
+ * @returns The MCP tool result.
28
+ */
29
+ export declare function validateTool(args: RecipeSourceArgs): Promise<CallToolResult>;
30
+ export {};
31
+ //# sourceMappingURL=validate-tool.handler.d.ts.map
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "@opencraw/mcp",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.esm.js",
6
+ "module": "./dist/index.esm.js",
7
+ "types": "./dist/src/index.d.ts",
8
+ "bin": {
9
+ "opencraw-mcp": "./bin/opencraw-mcp.mjs"
10
+ },
11
+ "exports": {
12
+ "./package.json": "./package.json",
13
+ ".": {
14
+ "types": "./dist/src/index.d.ts",
15
+ "import": "./dist/index.esm.js",
16
+ "default": "./dist/index.esm.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "bin",
22
+ "!**/*.tsbuildinfo",
23
+ "!**/*.d.ts.map",
24
+ "!**/*.js.map"
25
+ ],
26
+ "dependencies": {
27
+ "@opencraw/core": "^0.1.0",
28
+ "@opencraw/cli": "^0.1.0",
29
+ "@modelcontextprotocol/sdk": "^1.30.1",
30
+ "playwright": "^1.63.0",
31
+ "zod": "^4.6.5"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "description": "An MCP server exposing OpenCraw's crawler as agent tools: probe a page, validate recipes, run a crawl, list recipes.",
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/russoedu/open.craw.git",
41
+ "directory": "packages/mcp"
42
+ },
43
+ "keywords": [
44
+ "crawler",
45
+ "scraper",
46
+ "mcp",
47
+ "model-context-protocol",
48
+ "recipe"
49
+ ],
50
+ "engines": {
51
+ "node": ">=22"
52
+ },
53
+ "sideEffects": false,
54
+ "nx": {
55
+ "targets": {
56
+ "typecheck": {
57
+ "dependsOn": [
58
+ "^build"
59
+ ]
60
+ },
61
+ "lint": {
62
+ "dependsOn": [
63
+ "^build"
64
+ ]
65
+ },
66
+ "e2e": {
67
+ "executor": "nx:run-commands",
68
+ "dependsOn": [
69
+ "build",
70
+ {
71
+ "projects": [
72
+ "@opencraw/core"
73
+ ],
74
+ "target": "e2e"
75
+ },
76
+ {
77
+ "projects": [
78
+ "@opencraw/cli"
79
+ ],
80
+ "target": "e2e"
81
+ }
82
+ ],
83
+ "inputs": [
84
+ "default",
85
+ "^production",
86
+ "{workspaceRoot}/packages/core/e2e/**"
87
+ ],
88
+ "options": {
89
+ "command": "tsc -p tsconfig.e2e.json && jest --config jest.e2e.config.cts",
90
+ "cwd": "packages/mcp"
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }