@opencraw/mcp 0.1.47 → 0.1.48
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 +3 -2
- package/dist/index.esm.js +103 -16
- package/dist/src/studio-tool/index.d.ts +1 -1
- package/dist/src/studio-tool/studio-tools.handler.d.ts +21 -0
- package/dist/studio-tool/index.d.ts +1 -1
- package/dist/studio-tool/studio-tools.handler.d.ts +21 -0
- package/docs/studio/manual.md +3 -1
- package/package.json +2 -2
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
|
|
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
|
-
|
|
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
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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:
|
|
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:
|
|
673
|
-
|
|
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
|
|
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.
|
package/docs/studio/manual.md
CHANGED
|
@@ -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.
|
|
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.
|
|
3
|
+
"version": "0.1.48",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.esm.js",
|
|
6
6
|
"module": "./dist/index.esm.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@opencraw/core": "^0.1.42",
|
|
29
|
-
"@opencraw/cli": "^0.1.
|
|
29
|
+
"@opencraw/cli": "^0.1.48",
|
|
30
30
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
31
31
|
"playwright": "^1.63.0",
|
|
32
32
|
"zod": "^4.6.5"
|