@opencraw/mcp 0.1.47 → 0.1.49

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 CHANGED
@@ -152,10 +152,11 @@ Drive the OpenCraw Studio the person has open, so they watch the recipe being bu
152
152
  | Tool | Input | Returns |
153
153
  |---|---|---|
154
154
  | `studio_status` | none | `{ running, url, folder?, pages, running? }`: whether Studio is up, the folder it has open, how many connections are listening (an open page holds a couple), the recipe being sampled. Without a Studio: `{ running: false, hint }`. |
155
- | `studio_open_recipe` | `path` (a recipe file, a folder or a document), `workspace?` | `{ opened: true, pages }`, or `{ opened: false, hint }` when Studio runs but no page is open. The open page does it the way a Studio link does. |
155
+ | `studio_open_recipe` | `path` (a recipe file, a folder or a document), `workspace?` | `{ opened, message }` once the page has done it the way a Studio link does (or says why it could not); `{ opened: false, hint }` when Studio runs but no page is open. |
156
+ | `studio_pick` | `recipeId`, `selector?` or `text?`, `add?` | Picks a value on the recipe's web page as a click would: by CSS selector, or by the text the element shows. Answers `{ picked, message, text, selector, matches, alternatives }` (the selector chosen and the others offered, each with how many elements it matches). Left staged for the person to choose and press *Add to recipe*, or added to the recipe with `add`. Open the recipe first. |
156
157
  | `studio_run_sample` | `recipeId`, `maxRecords?` (10), `waitSeconds?` (60) | `{ recipeId, state, result? }`: the counts, the first 20 records and rejections once finished. Studio must have the recipe's folder open (`studio_open_recipe` first). |
157
158
 
158
- Picking a value on a page (`studio_pick`) is not here yet: it is still the person's click.
159
+ `studio_pick` is for web pages. For a PDF, a spreadsheet or a deck the person still clicks, and the agent tells them where.
159
160
 
160
161
  ## Building
161
162
 
package/dist/index.esm.js CHANGED
@@ -588,6 +588,13 @@ const studioOpenRecipeInputShape = {
588
588
  path: z.string().min(1).describe('A recipe file (`*.input.json` / `*.ocw-in.json`), a folder of recipes, or a document, as an absolute path.'),
589
589
  workspace: z.string().optional().describe('The folder a document belongs to, when `path` is a document.')
590
590
  };
591
+ /** Input schema for `studio_pick`. */
592
+ const studioPickInputShape = {
593
+ recipeId: z.string().min(1).describe('The id of an input recipe whose start point is a web page, in the folder Studio has open.'),
594
+ selector: z.string().optional().describe('A CSS selector of the element to pick on the page of the recipe. Give this or `text`.'),
595
+ text: z.string().optional().describe('The text the element shows, when you do not know a selector: the smallest element showing it is picked.'),
596
+ add: z.boolean().optional().describe('Add the pick to the recipe straight away (default: leave it staged, for the person to choose a selector and press Add to recipe).')
597
+ };
591
598
  /** Input schema for `studio_run_sample`. */
592
599
  const studioRunSampleInputShape = {
593
600
  recipeId: z.string().min(1).describe('The id of an input recipe in the folder Studio has open.'),
@@ -596,6 +603,8 @@ const studioRunSampleInputShape = {
596
603
  };
597
604
  const NO_STUDIO = 'No Studio is running. Start one with `opencraw studio <folder>`, or open a recipe from the OpenCraw view in VS Code ("Open in Studio"), then ask again.';
598
605
  const POLL_MS = 500;
606
+ /** How long a page gets to say how a request went. */
607
+ const REPORT_WAIT_MS = 30_000;
599
608
  function text(value, isError = false) {
600
609
  return {
601
610
  content: [{
@@ -642,6 +651,45 @@ async function studioStatusTool(directory) {
642
651
  return text(`Studio did not answer: ${messageOf(error)}`, true);
643
652
  }
644
653
  }
654
+ function sleep(ms) {
655
+ return new Promise(resolve => {
656
+ setTimeout(resolve, ms);
657
+ });
658
+ }
659
+ /** Asks the open page to do something and waits for what it reports. */
660
+ async function askPage(studio, request, waitMs = REPORT_WAIT_MS) {
661
+ const {
662
+ pages,
663
+ requestId
664
+ } = await sendStudioCommand(studio, {
665
+ type: 'ask-ui',
666
+ request
667
+ });
668
+ if (pages === 0) return {
669
+ pages
670
+ };
671
+ const deadline = Date.now() + waitMs;
672
+ while (Date.now() < deadline) {
673
+ const outcome = await sendStudioCommand(studio, {
674
+ type: 'ui-outcome',
675
+ requestId
676
+ });
677
+ if (outcome.state === 'done') return {
678
+ pages,
679
+ outcome: {
680
+ ok: outcome.ok === true,
681
+ message: outcome.message ?? '',
682
+ detail: outcome.detail
683
+ }
684
+ };
685
+ await sleep(POLL_MS / 5);
686
+ }
687
+ return {
688
+ pages
689
+ };
690
+ }
691
+ const NO_PAGE = 'Studio is running but no page is open on it. Ask the person to open Studio in their browser.';
692
+ const NO_REPORT = 'The page did not answer in time. Is the recipe in the folder Studio has open?';
645
693
  /**
646
694
  * `studio_open_recipe`: opens a recipe, a folder or a document in the Studio the person is looking at, so they watch the work happen.
647
695
  *
@@ -653,25 +701,62 @@ async function studioOpenRecipeTool(args, directory) {
653
701
  const studio = await studioOf(directory);
654
702
  if (studio === undefined) return text(NO_STUDIO, true);
655
703
  try {
656
- const {
657
- pages
658
- } = await sendStudioCommand(studio, {
659
- type: 'ask-ui',
660
- request: {
661
- kind: 'open',
662
- path: args.path,
663
- workspace: args.workspace
664
- }
704
+ const answer = await askPage(studio, {
705
+ kind: 'open',
706
+ path: args.path,
707
+ workspace: args.workspace
665
708
  });
666
- if (pages === 0) return text({
709
+ if (answer.pages === 0) return text({
667
710
  opened: false,
668
- hint: 'Studio is running but no page is open on it. Ask the person to open Studio in their browser.',
711
+ hint: NO_PAGE,
669
712
  url: studio.url
670
713
  });
714
+ if (answer.outcome === undefined) return text({
715
+ opened: false,
716
+ hint: NO_REPORT
717
+ });
671
718
  return text({
672
- opened: true,
673
- pages
719
+ opened: answer.outcome.ok,
720
+ message: answer.outcome.message
721
+ }, !answer.outcome.ok);
722
+ } catch (error) {
723
+ return text(`Studio refused: ${messageOf(error)}`, true);
724
+ }
725
+ }
726
+ /**
727
+ * `studio_pick`: picks a value on a recipe's web page in the Studio the person is looking at, as a click would: they see it selected and the
728
+ * ways to reach it. With `add` it is added to the recipe. Answers with the selector Studio chose and the others it offers.
729
+ *
730
+ * @param args - The recipe and what to pick (a selector or the text shown), and whether to add it.
731
+ * @param directory - Where Studios announce themselves.
732
+ * @returns The MCP tool result.
733
+ */
734
+ async function studioPickTool(args, directory) {
735
+ if (args.selector === undefined && args.text === undefined) return text('Give a CSS selector or the text the element shows.', true);
736
+ const studio = await studioOf(directory);
737
+ if (studio === undefined) return text(NO_STUDIO, true);
738
+ try {
739
+ const answer = await askPage(studio, {
740
+ kind: 'pick',
741
+ recipeId: args.recipeId,
742
+ selector: args.selector,
743
+ text: args.text,
744
+ add: args.add
674
745
  });
746
+ if (answer.pages === 0) return text({
747
+ picked: false,
748
+ hint: NO_PAGE,
749
+ url: studio.url
750
+ }, true);
751
+ if (answer.outcome === undefined) return text({
752
+ picked: false,
753
+ hint: NO_REPORT
754
+ }, true);
755
+ return text({
756
+ picked: answer.outcome.ok,
757
+ message: answer.outcome.message,
758
+ ...answer.outcome.detail
759
+ }, !answer.outcome.ok);
675
760
  } catch (error) {
676
761
  return text(`Studio refused: ${messageOf(error)}`, true);
677
762
  }
@@ -712,9 +797,7 @@ async function studioRunSampleTool(args, directory) {
712
797
  ...status,
713
798
  hint: 'Still running; ask again with studio_run_sample or look in Studio.'
714
799
  });
715
- await new Promise(resolve => {
716
- setTimeout(resolve, POLL_MS);
717
- });
800
+ await sleep(POLL_MS);
718
801
  }
719
802
  } catch (error) {
720
803
  return text(`Studio could not run it: ${messageOf(error)}`, true);
@@ -861,6 +944,10 @@ function createServer() {
861
944
  description: 'Open a recipe, a folder of recipes or a document in the Studio the person has open, so they watch the recipe being built. Does nothing to the files.',
862
945
  inputSchema: studioOpenRecipeInputShape
863
946
  }, args => studioOpenRecipeTool(args));
947
+ server.registerTool('studio_pick', {
948
+ description: 'Pick a value on the web page of an input recipe in Studio, as a click would: give a CSS selector or the text the element shows. The person sees it selected and the ways to reach it; with add it is added to the recipe. Returns the selector chosen and the alternatives, with how many elements each matches. Open the recipe first with studio_open_recipe.',
949
+ inputSchema: studioPickInputShape
950
+ }, args => studioPickTool(args));
864
951
  server.registerTool('studio_run_sample', {
865
952
  description: 'Run an input recipe as a sample in Studio (the person sees the records arrive) and get back the counts, the first records and what was rejected. Studio must have the folder of that recipe open: call studio_open_recipe first.',
866
953
  inputSchema: studioRunSampleInputShape
@@ -1,2 +1,2 @@
1
- export { studioOpenRecipeInputShape, studioOpenRecipeTool, studioRunSampleInputShape, studioRunSampleTool, studioStatusInputShape, studioStatusTool } from './studio-tools.handler.js';
1
+ export { studioOpenRecipeInputShape, studioOpenRecipeTool, studioPickInputShape, studioPickTool, studioRunSampleInputShape, studioRunSampleTool, studioStatusInputShape, studioStatusTool } from './studio-tools.handler.js';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -7,6 +7,13 @@ export declare const studioOpenRecipeInputShape: {
7
7
  path: z.ZodString;
8
8
  workspace: z.ZodOptional<z.ZodString>;
9
9
  };
10
+ /** Input schema for `studio_pick`. */
11
+ export declare const studioPickInputShape: {
12
+ recipeId: z.ZodString;
13
+ selector: z.ZodOptional<z.ZodString>;
14
+ text: z.ZodOptional<z.ZodString>;
15
+ add: z.ZodOptional<z.ZodBoolean>;
16
+ };
10
17
  /** Input schema for `studio_run_sample`. */
11
18
  export declare const studioRunSampleInputShape: {
12
19
  recipeId: z.ZodString;
@@ -31,6 +38,20 @@ export declare function studioOpenRecipeTool(args: {
31
38
  path: string;
32
39
  workspace?: string;
33
40
  }, directory?: string): Promise<CallToolResult>;
41
+ /**
42
+ * `studio_pick`: picks a value on a recipe's web page in the Studio the person is looking at, as a click would: they see it selected and the
43
+ * ways to reach it. With `add` it is added to the recipe. Answers with the selector Studio chose and the others it offers.
44
+ *
45
+ * @param args - The recipe and what to pick (a selector or the text shown), and whether to add it.
46
+ * @param directory - Where Studios announce themselves.
47
+ * @returns The MCP tool result.
48
+ */
49
+ export declare function studioPickTool(args: {
50
+ recipeId: string;
51
+ selector?: string;
52
+ text?: string;
53
+ add?: boolean;
54
+ }, directory?: string): Promise<CallToolResult>;
34
55
  /**
35
56
  * `studio_run_sample`: runs an input recipe as a sample in Studio (the person sees the records arrive), waits for it, and returns the
36
57
  * counts and the first records.
@@ -1,2 +1,2 @@
1
- export { studioOpenRecipeInputShape, studioOpenRecipeTool, studioRunSampleInputShape, studioRunSampleTool, studioStatusInputShape, studioStatusTool } from './studio-tools.handler';
1
+ export { studioOpenRecipeInputShape, studioOpenRecipeTool, studioPickInputShape, studioPickTool, studioRunSampleInputShape, studioRunSampleTool, studioStatusInputShape, studioStatusTool } from './studio-tools.handler';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -7,6 +7,13 @@ export declare const studioOpenRecipeInputShape: {
7
7
  path: z.ZodString;
8
8
  workspace: z.ZodOptional<z.ZodString>;
9
9
  };
10
+ /** Input schema for `studio_pick`. */
11
+ export declare const studioPickInputShape: {
12
+ recipeId: z.ZodString;
13
+ selector: z.ZodOptional<z.ZodString>;
14
+ text: z.ZodOptional<z.ZodString>;
15
+ add: z.ZodOptional<z.ZodBoolean>;
16
+ };
10
17
  /** Input schema for `studio_run_sample`. */
11
18
  export declare const studioRunSampleInputShape: {
12
19
  recipeId: z.ZodString;
@@ -31,6 +38,20 @@ export declare function studioOpenRecipeTool(args: {
31
38
  path: string;
32
39
  workspace?: string;
33
40
  }, directory?: string): Promise<CallToolResult>;
41
+ /**
42
+ * `studio_pick`: picks a value on a recipe's web page in the Studio the person is looking at, as a click would: they see it selected and the
43
+ * ways to reach it. With `add` it is added to the recipe. Answers with the selector Studio chose and the others it offers.
44
+ *
45
+ * @param args - The recipe and what to pick (a selector or the text shown), and whether to add it.
46
+ * @param directory - Where Studios announce themselves.
47
+ * @returns The MCP tool result.
48
+ */
49
+ export declare function studioPickTool(args: {
50
+ recipeId: string;
51
+ selector?: string;
52
+ text?: string;
53
+ add?: boolean;
54
+ }, directory?: string): Promise<CallToolResult>;
34
55
  /**
35
56
  * `studio_run_sample`: runs an input recipe as a sample in Studio (the person sees the records arrive), waits for it, and returns the
36
57
  * counts and the first records.
@@ -735,7 +735,7 @@ local `file:…xlsx` / `…xlsm`) reads every worksheet with
735
735
  `2026-06-01T09:30:00`), errors their text (`#DIV/0!`), and empty cells `''`. Formulas give their cached
736
736
  value; nothing is evaluated, and display formats are not applied (a percentage stays `0.125`). Hidden sheets
737
737
  and rows are read, and `table` skips them unless `includeHidden`. Merged ranges are recorded, and `table`
738
- fills them. A legacy `.xls`, a password-protected file or an `.ods` fails the step, with what to do.
738
+ fills them. An OpenDocument spreadsheet (`.ods`) reads exactly like an `.xlsx`, by its extension, its content type or the `mimetype` the file declares. A legacy `.xls` or a password-protected file fails the step, with what to do.
739
739
 
740
740
  **CSV.** `request` with `as: "csv"` (or a response served as `text/csv`, `application/csv` or
741
741
  `text/tab-separated-values`, or a local `file:…csv` / `…tsv`) reads the file into a **workbook**: a CSV is a
@@ -305,8 +305,10 @@ happen instead of reading a report:
305
305
 
306
306
  - **`studio_status`** tells it whether Studio is running, which folder it has open and what it is sampling.
307
307
  - **`studio_open_recipe`** opens a recipe, a folder or a document in your Studio, as if you had followed a link to it.
308
+ - **`studio_pick`** picks a value on a web page for you to see: by CSS selector or by the text it shows. It is staged the way your own click is, with the
309
+ ways to find it to choose from, and the agent can add it to the recipe.
308
310
  - **`studio_run_sample`** runs an input recipe as a sample: the records arrive in your Records tab, and the agent gets the counts and the first
309
311
  records back.
310
312
 
311
313
  Nothing needs setting up: a running Studio announces itself in `~/.opencraw/studio` (its address and its access token, readable only by you), and the
312
- agent finds it there. Studio still listens on this machine only. Clicking a value on a page or document stays your click: the agent tells you where.
314
+ agent finds it there. Studio still listens on this machine only. On a PDF, a spreadsheet or a deck the picking is still your click: the agent tells you where.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opencraw/mcp",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
4
4
  "type": "module",
5
5
  "main": "./dist/index.esm.js",
6
6
  "module": "./dist/index.esm.js",
@@ -25,8 +25,8 @@
25
25
  "!**/*.js.map"
26
26
  ],
27
27
  "dependencies": {
28
- "@opencraw/core": "^0.1.42",
29
- "@opencraw/cli": "^0.1.47",
28
+ "@opencraw/core": "^0.1.43",
29
+ "@opencraw/cli": "^0.1.49",
30
30
  "@modelcontextprotocol/sdk": "^1.32.1",
31
31
  "playwright": "^1.63.0",
32
32
  "zod": "^4.6.5"