@selextract/mcp-selextract 0.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.
- package/README.md +63 -0
- package/dist/cli.js +28 -0
- package/dist/config.js +40 -0
- package/dist/http.js +47 -0
- package/dist/resources.js +50 -0
- package/dist/serialize.js +7 -0
- package/dist/server.js +62 -0
- package/dist/tools.js +664 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Selextract MCP Server (local stdio)
|
|
2
|
+
|
|
3
|
+
This is a local `stdio` MCP server that lets MCP-capable clients call Selextract Cloud (the Worker API) via HTTP.
|
|
4
|
+
|
|
5
|
+
If you can run `npx`, you can add it to your MCP client with one config entry.
|
|
6
|
+
|
|
7
|
+
## Env
|
|
8
|
+
|
|
9
|
+
- `SELEXTRACT_API_URL`
|
|
10
|
+
- Examples: `http://localhost:8246`, `https://api.selextract.com`, `https://api.selextract.com/api`
|
|
11
|
+
- This server normalizes it to end with `/api` (if you pass `/api/v1`, it will trim back to `/api`).
|
|
12
|
+
- `SELEXTRACT_API_KEY` (your `sk_...` user API key)
|
|
13
|
+
- Legacy alias: `SELEXTRACT_API_TOKEN`
|
|
14
|
+
- Optional: `SELEXTRACT_TIMEOUT_MS` (default: `30000`)
|
|
15
|
+
- Optional: `SELEXTRACT_MAX_RESPONSE_CHARS` (default: `30000`)
|
|
16
|
+
- Optional: `SELEXTRACT_ENV_FILE` (default: `.env`)
|
|
17
|
+
|
|
18
|
+
## Run (dev)
|
|
19
|
+
|
|
20
|
+
From `selextract-cloud/`:
|
|
21
|
+
|
|
22
|
+
`pnpm --filter @selextract/mcp-selextract dev`
|
|
23
|
+
|
|
24
|
+
## MCP config example (`mcp.json`)
|
|
25
|
+
|
|
26
|
+
Add this to your MCP config (keep keys in env vars if possible):
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"selextract-cloud": {
|
|
32
|
+
"command": "npx",
|
|
33
|
+
"args": ["-y", "--package", "@selextract/mcp-selextract", "mcp-selextract"],
|
|
34
|
+
"env": {
|
|
35
|
+
"SELEXTRACT_API_URL": "http://localhost:8246",
|
|
36
|
+
"SELEXTRACT_API_KEY": "sk_REPLACE_ME"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## What you can do
|
|
44
|
+
|
|
45
|
+
Typical flow:
|
|
46
|
+
|
|
47
|
+
1. `task_create` → returns `preview_id`
|
|
48
|
+
2. `task_build_status` → poll until status is `complete` (or `failed`)
|
|
49
|
+
3. `task_publish` → turns the draft into a saved task (`task_id`)
|
|
50
|
+
4. `run_create` → runs the task (`run_id`)
|
|
51
|
+
5. `run_results` → reads results (paginated)
|
|
52
|
+
|
|
53
|
+
Useful extras:
|
|
54
|
+
|
|
55
|
+
- Draft cleanup: `task_draft_delete`
|
|
56
|
+
- Task repair (self-healing rebuild): `task_repair`
|
|
57
|
+
- Recipe versioning (rollback/switch): `task_recipe_versions`, `task_set_recipe_version`
|
|
58
|
+
- Authenticated scraping: access profile tools (create/list/update/delete/build-session)
|
|
59
|
+
- Run lifecycle: `run_get`, `run_list`, `run_stop`, `run_delete`
|
|
60
|
+
|
|
61
|
+
## Resources (read-only)
|
|
62
|
+
|
|
63
|
+
- `selextract://help` (usage guide)
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { readEnv } from './config.js';
|
|
5
|
+
import { SelextractApiClient } from './http.js';
|
|
6
|
+
import { createSelextractMcpServer } from './server.js';
|
|
7
|
+
async function main() {
|
|
8
|
+
// Best-effort .env loading for local dev; users can omit or override with SELEXTRACT_ENV_FILE.
|
|
9
|
+
const envFile = process.env.SELEXTRACT_ENV_FILE ?? '.env';
|
|
10
|
+
dotenv.config({ path: envFile });
|
|
11
|
+
const env = readEnv(process.env);
|
|
12
|
+
const api = new SelextractApiClient({
|
|
13
|
+
baseUrl: env.SELEXTRACT_API_URL,
|
|
14
|
+
apiKey: env.SELEXTRACT_API_KEY,
|
|
15
|
+
timeoutMs: env.SELEXTRACT_TIMEOUT_MS,
|
|
16
|
+
});
|
|
17
|
+
const server = createSelextractMcpServer({
|
|
18
|
+
api,
|
|
19
|
+
maxResponseChars: env.SELEXTRACT_MAX_RESPONSE_CHARS,
|
|
20
|
+
});
|
|
21
|
+
const transport = new StdioServerTransport();
|
|
22
|
+
await server.connect(transport);
|
|
23
|
+
}
|
|
24
|
+
main().catch((err) => {
|
|
25
|
+
// Stdio MCP servers should only write to stderr.
|
|
26
|
+
process.stderr.write(`${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
});
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const EnvSchema = z.object({
|
|
3
|
+
SELEXTRACT_API_URL: z.string().min(1),
|
|
4
|
+
SELEXTRACT_API_KEY: z.string().min(1).optional(),
|
|
5
|
+
// Legacy alias (kept for older configs).
|
|
6
|
+
SELEXTRACT_API_TOKEN: z.string().min(1).optional(),
|
|
7
|
+
SELEXTRACT_TIMEOUT_MS: z.coerce.number().int().positive().optional().default(30000),
|
|
8
|
+
SELEXTRACT_MAX_RESPONSE_CHARS: z.coerce.number().int().positive().optional().default(30000),
|
|
9
|
+
SELEXTRACT_ENV_FILE: z.string().optional().default('.env'),
|
|
10
|
+
});
|
|
11
|
+
function normalizeApiUrl(raw) {
|
|
12
|
+
const url = new URL(raw);
|
|
13
|
+
const pathSegments = url.pathname.split('/').filter(Boolean);
|
|
14
|
+
const apiIndex = pathSegments.indexOf('api');
|
|
15
|
+
if (apiIndex === -1) {
|
|
16
|
+
url.pathname = pathSegments.length === 0 ? '/api' : `/${pathSegments.join('/')}/api`;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
// If the user passes .../api/v1 (or anything after /api), normalize to .../api.
|
|
20
|
+
url.pathname = `/${pathSegments.slice(0, apiIndex + 1).join('/')}`;
|
|
21
|
+
}
|
|
22
|
+
return url.toString().replace(/\/+$/, '');
|
|
23
|
+
}
|
|
24
|
+
export function readEnv(processEnv) {
|
|
25
|
+
const parsed = EnvSchema.safeParse(processEnv);
|
|
26
|
+
if (!parsed.success) {
|
|
27
|
+
const issues = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
|
|
28
|
+
throw new Error(`Invalid environment: ${issues}`);
|
|
29
|
+
}
|
|
30
|
+
const { SELEXTRACT_API_KEY, SELEXTRACT_API_TOKEN, ...rest } = parsed.data;
|
|
31
|
+
const apiKey = SELEXTRACT_API_KEY ?? SELEXTRACT_API_TOKEN;
|
|
32
|
+
if (!apiKey) {
|
|
33
|
+
throw new Error('Invalid environment: SELEXTRACT_API_KEY (or legacy SELEXTRACT_API_TOKEN) is required');
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
...rest,
|
|
37
|
+
SELEXTRACT_API_KEY: apiKey,
|
|
38
|
+
SELEXTRACT_API_URL: normalizeApiUrl(rest.SELEXTRACT_API_URL),
|
|
39
|
+
};
|
|
40
|
+
}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export class SelextractApiClient {
|
|
2
|
+
constructor(opts) {
|
|
3
|
+
this.baseUrl = new URL(opts.baseUrl.endsWith('/') ? opts.baseUrl : `${opts.baseUrl}/`);
|
|
4
|
+
this.apiKey = opts.apiKey.trim();
|
|
5
|
+
this.timeoutMs = opts.timeoutMs;
|
|
6
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
7
|
+
}
|
|
8
|
+
async request(opts) {
|
|
9
|
+
const url = new URL(opts.path.replace(/^\//, ''), this.baseUrl);
|
|
10
|
+
if (opts.query) {
|
|
11
|
+
for (const [key, value] of Object.entries(opts.query)) {
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
continue;
|
|
14
|
+
url.searchParams.set(key, String(value));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
19
|
+
try {
|
|
20
|
+
const resp = await this.fetchImpl(url.toString(), {
|
|
21
|
+
method: opts.method,
|
|
22
|
+
headers: {
|
|
23
|
+
accept: 'application/json',
|
|
24
|
+
'content-type': 'application/json',
|
|
25
|
+
// Worker accepts Bearer JWT or `sk_...` api keys; it treats `sk_...` as an API key.
|
|
26
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
27
|
+
},
|
|
28
|
+
body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
|
|
29
|
+
signal: controller.signal,
|
|
30
|
+
});
|
|
31
|
+
const contentType = resp.headers.get('content-type') ?? '';
|
|
32
|
+
const isJson = contentType.includes('application/json');
|
|
33
|
+
const payload = isJson ? await resp.json().catch(() => null) : await resp.text().catch(() => '');
|
|
34
|
+
if (!resp.ok) {
|
|
35
|
+
const message = typeof payload?.message === 'string'
|
|
36
|
+
? String(payload.message)
|
|
37
|
+
: `Request failed (${resp.status})`;
|
|
38
|
+
const err = { status: resp.status, message, payload };
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
return payload;
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
clearTimeout(timeout);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
export function resourceDefinitions() {
|
|
3
|
+
return {
|
|
4
|
+
resources: [
|
|
5
|
+
{
|
|
6
|
+
uri: 'selextract://help',
|
|
7
|
+
name: 'Help',
|
|
8
|
+
description: 'Quick usage guide for this MCP server.',
|
|
9
|
+
mimeType: 'text/markdown',
|
|
10
|
+
},
|
|
11
|
+
],
|
|
12
|
+
resourceTemplates: [],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export async function readResource(opts) {
|
|
16
|
+
let url;
|
|
17
|
+
try {
|
|
18
|
+
url = new URL(opts.uri);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid resource URI: ${opts.uri}`);
|
|
22
|
+
}
|
|
23
|
+
if (url.protocol !== 'selextract:') {
|
|
24
|
+
throw new McpError(ErrorCode.InvalidParams, `Unsupported resource scheme: ${url.protocol}`);
|
|
25
|
+
}
|
|
26
|
+
const pathParts = url.pathname.replace(/\/+$/, '').split('/').filter(Boolean);
|
|
27
|
+
const parts = (url.hostname ? [url.hostname, ...pathParts] : pathParts).filter(Boolean);
|
|
28
|
+
if (parts.length === 1 && parts[0] === 'help') {
|
|
29
|
+
const value = [
|
|
30
|
+
'# Selextract MCP Server',
|
|
31
|
+
'',
|
|
32
|
+
'This MCP server exposes only **basic** operations for:',
|
|
33
|
+
'- Creating AI-built tasks (draft → publish)',
|
|
34
|
+
'- Repairing tasks (rebuild recipe)',
|
|
35
|
+
'- Switching task recipe versions (rollback)',
|
|
36
|
+
'- Running tasks (create/get/list/stop/delete runs)',
|
|
37
|
+
'- Managing access profiles (for logged-in/session scraping)',
|
|
38
|
+
'',
|
|
39
|
+
'It intentionally does **not** expose trace/scratchpad or other deep debugging data.',
|
|
40
|
+
'',
|
|
41
|
+
'Required env vars:',
|
|
42
|
+
'- SELEXTRACT_API_URL',
|
|
43
|
+
'- SELEXTRACT_API_KEY (or legacy SELEXTRACT_API_TOKEN)',
|
|
44
|
+
].join('\n');
|
|
45
|
+
return {
|
|
46
|
+
contents: [{ uri: opts.uri, mimeType: 'text/markdown', text: value }],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown resource: ${opts.uri}`);
|
|
50
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export function jsonStringifyLimited(value, maxChars) {
|
|
2
|
+
const raw = JSON.stringify(value, null, 2);
|
|
3
|
+
if (raw.length <= maxChars)
|
|
4
|
+
return { text: raw, truncated: false };
|
|
5
|
+
const head = raw.slice(0, Math.max(0, maxChars - 40));
|
|
6
|
+
return { text: `${head}\n...TRUNCATED (${raw.length} chars total)`, truncated: true };
|
|
7
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { CallToolRequestSchema, ErrorCode, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { toolDefinitions, toolHandlers } from './tools.js';
|
|
5
|
+
import { readResource, resourceDefinitions } from './resources.js';
|
|
6
|
+
function readPackageVersion() {
|
|
7
|
+
try {
|
|
8
|
+
const pkgUrl = new URL('../package.json', import.meta.url);
|
|
9
|
+
const raw = readFileSync(pkgUrl, 'utf8');
|
|
10
|
+
const parsed = JSON.parse(raw);
|
|
11
|
+
return typeof parsed?.version === 'string' ? parsed.version : '0.0.0';
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return '0.0.0';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function createSelextractMcpServer(opts) {
|
|
18
|
+
const server = new Server({ name: 'selextract-cloud', version: readPackageVersion() }, {
|
|
19
|
+
capabilities: {
|
|
20
|
+
tools: {},
|
|
21
|
+
resources: {},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
const defs = toolDefinitions();
|
|
25
|
+
const handlers = toolHandlers(opts.api, opts.maxResponseChars);
|
|
26
|
+
const resources = resourceDefinitions();
|
|
27
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
28
|
+
return {
|
|
29
|
+
tools: defs.map((tool) => ({
|
|
30
|
+
name: tool.name,
|
|
31
|
+
description: tool.description,
|
|
32
|
+
inputSchema: tool.inputSchema,
|
|
33
|
+
})),
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
37
|
+
return { resources: resources.resources };
|
|
38
|
+
});
|
|
39
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
40
|
+
return { resourceTemplates: resources.resourceTemplates };
|
|
41
|
+
});
|
|
42
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
43
|
+
const uri = String(request.params?.uri ?? '');
|
|
44
|
+
if (!uri) {
|
|
45
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing uri');
|
|
46
|
+
}
|
|
47
|
+
return await readResource({
|
|
48
|
+
api: opts.api,
|
|
49
|
+
maxResponseChars: opts.maxResponseChars,
|
|
50
|
+
uri,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
54
|
+
const name = String(request.params?.name ?? '');
|
|
55
|
+
const args = request.params?.arguments;
|
|
56
|
+
if (!name || !(name in handlers)) {
|
|
57
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name || '(empty)'}`);
|
|
58
|
+
}
|
|
59
|
+
return await handlers[name](args);
|
|
60
|
+
});
|
|
61
|
+
return server;
|
|
62
|
+
}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { jsonStringifyLimited } from './serialize.js';
|
|
3
|
+
function asTextResult(value, maxChars) {
|
|
4
|
+
const { text, truncated } = jsonStringifyLimited(value, maxChars);
|
|
5
|
+
return {
|
|
6
|
+
content: [
|
|
7
|
+
{
|
|
8
|
+
type: 'text',
|
|
9
|
+
text: truncated ? `${text}\n\nNote: Increase SELEXTRACT_MAX_RESPONSE_CHARS to see more.` : text,
|
|
10
|
+
},
|
|
11
|
+
],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const FieldSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
name: z.string().min(1).optional(),
|
|
17
|
+
value: z.string().min(1).optional(),
|
|
18
|
+
type: z.string().optional(),
|
|
19
|
+
required: z.boolean().optional(),
|
|
20
|
+
examples: z.array(z.string()).optional(),
|
|
21
|
+
})
|
|
22
|
+
.refine((data) => Boolean(data.name?.trim() || data.value?.trim()), {
|
|
23
|
+
message: 'Provide a field name (name) or a short description (value).',
|
|
24
|
+
path: ['value'],
|
|
25
|
+
});
|
|
26
|
+
const TaskCreateInputSchema = z.preprocess((raw) => {
|
|
27
|
+
if (!raw || typeof raw !== 'object')
|
|
28
|
+
return raw;
|
|
29
|
+
const value = raw;
|
|
30
|
+
return {
|
|
31
|
+
...value,
|
|
32
|
+
access_profile_id: value.access_profile_id ?? value.accessProfileId,
|
|
33
|
+
field_mode: value.field_mode ?? value.fieldMode ?? value.mode,
|
|
34
|
+
build_mode: value.build_mode ?? value.buildMode,
|
|
35
|
+
script_source: value.script_source ?? value.scriptSource,
|
|
36
|
+
recipe_override: value.recipe_override ?? value.recipeOverride,
|
|
37
|
+
max_preview_rows: value.max_preview_rows ?? value.maxPreviewRows,
|
|
38
|
+
goal: value.goal ?? value.description,
|
|
39
|
+
};
|
|
40
|
+
}, z
|
|
41
|
+
.object({
|
|
42
|
+
url: z.string().url(),
|
|
43
|
+
access_profile_id: z.string().uuid().optional(),
|
|
44
|
+
field_mode: z.enum(['auto', 'manual']).optional(),
|
|
45
|
+
build_mode: z.enum(['selectors', 'flow', 'code', 'auto']).optional().default('auto'),
|
|
46
|
+
script_source: z.string().min(1).optional(),
|
|
47
|
+
recipe_override: z.any().optional(),
|
|
48
|
+
goal: z.string().min(1).optional(),
|
|
49
|
+
fields: z.array(FieldSchema).optional(),
|
|
50
|
+
max_preview_rows: z.number().int().positive().optional().default(10),
|
|
51
|
+
advanced: z.boolean().optional().default(false),
|
|
52
|
+
})
|
|
53
|
+
.superRefine((data, ctx) => {
|
|
54
|
+
const effectiveFieldMode = data.field_mode ?? (data.fields?.length ? 'manual' : 'auto');
|
|
55
|
+
if (effectiveFieldMode === 'manual' && (!data.fields || data.fields.length === 0)) {
|
|
56
|
+
ctx.addIssue({
|
|
57
|
+
code: z.ZodIssueCode.custom,
|
|
58
|
+
message: 'In manual mode, fields must be provided.',
|
|
59
|
+
path: ['fields'],
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}));
|
|
63
|
+
const TaskBuildStatusInputSchema = z.object({
|
|
64
|
+
preview_id: z.string().uuid(),
|
|
65
|
+
});
|
|
66
|
+
const TaskPublishInputSchema = z.preprocess((raw) => {
|
|
67
|
+
if (!raw || typeof raw !== 'object')
|
|
68
|
+
return raw;
|
|
69
|
+
const value = raw;
|
|
70
|
+
return {
|
|
71
|
+
...value,
|
|
72
|
+
access_profile_id: value.access_profile_id ?? value.accessProfileId,
|
|
73
|
+
};
|
|
74
|
+
}, z.object({
|
|
75
|
+
preview_id: z.string().uuid(),
|
|
76
|
+
name: z.string().min(1).max(255),
|
|
77
|
+
access_profile_id: z.string().uuid().optional(),
|
|
78
|
+
}));
|
|
79
|
+
const TaskDraftDeleteInputSchema = z.object({
|
|
80
|
+
preview_id: z.string().uuid(),
|
|
81
|
+
});
|
|
82
|
+
const TaskRepairInputSchema = z.preprocess((raw) => {
|
|
83
|
+
if (!raw || typeof raw !== 'object')
|
|
84
|
+
return raw;
|
|
85
|
+
const value = raw;
|
|
86
|
+
return {
|
|
87
|
+
...value,
|
|
88
|
+
hint: value.hint ?? value.what_is_wrong ?? value.whatIsWrong ?? value.issue,
|
|
89
|
+
};
|
|
90
|
+
}, z
|
|
91
|
+
.object({
|
|
92
|
+
task_id: z.string().uuid(),
|
|
93
|
+
hint: z.string().min(1).max(2000).optional(),
|
|
94
|
+
force: z.boolean().optional().default(false),
|
|
95
|
+
})
|
|
96
|
+
.passthrough());
|
|
97
|
+
const TaskRecipeVersionsInputSchema = z.object({
|
|
98
|
+
task_id: z.string().uuid(),
|
|
99
|
+
});
|
|
100
|
+
const TaskSetRecipeVersionInputSchema = z
|
|
101
|
+
.object({
|
|
102
|
+
task_id: z.string().uuid(),
|
|
103
|
+
recipe_version_id: z.string().uuid().optional(),
|
|
104
|
+
version: z.coerce.number().int().positive().optional(),
|
|
105
|
+
})
|
|
106
|
+
.superRefine((data, ctx) => {
|
|
107
|
+
if (!data.recipe_version_id && !data.version) {
|
|
108
|
+
ctx.addIssue({
|
|
109
|
+
code: z.ZodIssueCode.custom,
|
|
110
|
+
message: 'Provide recipe_version_id or version.',
|
|
111
|
+
path: ['recipe_version_id'],
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
const AccessProfileHeaderSchema = z.object({
|
|
116
|
+
name: z.string().min(1),
|
|
117
|
+
value: z.string().min(1),
|
|
118
|
+
});
|
|
119
|
+
const AccessProfileInputSchema = z.object({
|
|
120
|
+
key: z.string().min(1),
|
|
121
|
+
label: z.string().min(1).optional(),
|
|
122
|
+
type: z.enum(['text', 'password']).optional(),
|
|
123
|
+
value: z.string().min(1),
|
|
124
|
+
});
|
|
125
|
+
const AccessProfileCreateInputSchema = z.preprocess((raw) => {
|
|
126
|
+
if (!raw || typeof raw !== 'object')
|
|
127
|
+
return raw;
|
|
128
|
+
const value = raw;
|
|
129
|
+
const login = value.login && typeof value.login === 'object' ? value.login : undefined;
|
|
130
|
+
return {
|
|
131
|
+
...value,
|
|
132
|
+
canonical_domain: value.canonical_domain ?? value.canonicalDomain,
|
|
133
|
+
storage_state: value.storage_state ?? value.storageState,
|
|
134
|
+
login: login
|
|
135
|
+
? {
|
|
136
|
+
...login,
|
|
137
|
+
start_url: login.start_url ?? login.startUrl,
|
|
138
|
+
}
|
|
139
|
+
: value.login,
|
|
140
|
+
};
|
|
141
|
+
}, z.object({
|
|
142
|
+
name: z.string().min(1).max(255),
|
|
143
|
+
url: z.string().url().optional(),
|
|
144
|
+
canonical_domain: z.string().min(1).optional(),
|
|
145
|
+
kind: z.enum(['custom', 'credentials', 'session']).optional(),
|
|
146
|
+
headers: z.array(AccessProfileHeaderSchema).optional(),
|
|
147
|
+
inputs: z.array(AccessProfileInputSchema).optional(),
|
|
148
|
+
storage_state: z.unknown().optional(),
|
|
149
|
+
login: z
|
|
150
|
+
.object({
|
|
151
|
+
start_url: z.string().url().optional(),
|
|
152
|
+
hint: z.string().min(1).optional(),
|
|
153
|
+
})
|
|
154
|
+
.optional(),
|
|
155
|
+
}));
|
|
156
|
+
const AccessProfileUpdateInputSchema = z.preprocess((raw) => {
|
|
157
|
+
if (!raw || typeof raw !== 'object')
|
|
158
|
+
return raw;
|
|
159
|
+
const value = raw;
|
|
160
|
+
if (value.patch && typeof value.patch === 'object')
|
|
161
|
+
return value;
|
|
162
|
+
const { access_profile_id, ...patch } = value;
|
|
163
|
+
return { access_profile_id, patch };
|
|
164
|
+
}, z.object({
|
|
165
|
+
access_profile_id: z.string().uuid(),
|
|
166
|
+
patch: z.record(z.unknown()).optional().default({}),
|
|
167
|
+
}));
|
|
168
|
+
const AccessProfileBuildSessionInputSchema = z.preprocess((raw) => {
|
|
169
|
+
if (!raw || typeof raw !== 'object')
|
|
170
|
+
return raw;
|
|
171
|
+
const value = raw;
|
|
172
|
+
return {
|
|
173
|
+
...value,
|
|
174
|
+
start_url: value.start_url ?? value.startUrl,
|
|
175
|
+
};
|
|
176
|
+
}, z.object({
|
|
177
|
+
access_profile_id: z.string().uuid(),
|
|
178
|
+
start_url: z.string().url().optional(),
|
|
179
|
+
hint: z.string().min(1).optional(),
|
|
180
|
+
}));
|
|
181
|
+
export const ToolInputs = {
|
|
182
|
+
health: z.object({}),
|
|
183
|
+
// Tasks (AI-built)
|
|
184
|
+
task_create: TaskCreateInputSchema,
|
|
185
|
+
task_build_status: TaskBuildStatusInputSchema,
|
|
186
|
+
task_publish: TaskPublishInputSchema,
|
|
187
|
+
task_draft_delete: TaskDraftDeleteInputSchema,
|
|
188
|
+
task_repair: TaskRepairInputSchema,
|
|
189
|
+
task_recipe_versions: TaskRecipeVersionsInputSchema,
|
|
190
|
+
task_set_recipe_version: TaskSetRecipeVersionInputSchema,
|
|
191
|
+
// Runs
|
|
192
|
+
run_create: z.object({
|
|
193
|
+
task_id: z.string().uuid(),
|
|
194
|
+
max_runtime_seconds: z.number().int().min(30).max(3600).optional(),
|
|
195
|
+
}),
|
|
196
|
+
run_get: z.object({
|
|
197
|
+
run_id: z.string().uuid(),
|
|
198
|
+
}),
|
|
199
|
+
run_list: z.object({
|
|
200
|
+
task_id: z.string().uuid(),
|
|
201
|
+
limit: z.number().int().min(1).max(100).optional().default(20),
|
|
202
|
+
}),
|
|
203
|
+
run_stop: z.object({
|
|
204
|
+
run_id: z.string().uuid(),
|
|
205
|
+
}),
|
|
206
|
+
run_delete: z.object({
|
|
207
|
+
run_id: z.string().uuid(),
|
|
208
|
+
}),
|
|
209
|
+
run_results: z.object({
|
|
210
|
+
run_id: z.string().uuid(),
|
|
211
|
+
limit: z.number().int().min(1).max(1000).optional().default(100),
|
|
212
|
+
cursor: z.string().optional(),
|
|
213
|
+
offset: z.number().int().min(0).optional(),
|
|
214
|
+
}),
|
|
215
|
+
// Access profiles (authenticated scraping)
|
|
216
|
+
access_profile_list: z.object({}),
|
|
217
|
+
access_profile_get: z.object({
|
|
218
|
+
access_profile_id: z.string().uuid(),
|
|
219
|
+
}),
|
|
220
|
+
access_profile_create: AccessProfileCreateInputSchema,
|
|
221
|
+
access_profile_update: AccessProfileUpdateInputSchema,
|
|
222
|
+
access_profile_delete: z.object({
|
|
223
|
+
access_profile_id: z.string().uuid(),
|
|
224
|
+
}),
|
|
225
|
+
access_profile_build_session: AccessProfileBuildSessionInputSchema,
|
|
226
|
+
};
|
|
227
|
+
export function toolDefinitions() {
|
|
228
|
+
return [
|
|
229
|
+
{
|
|
230
|
+
name: 'health',
|
|
231
|
+
description: 'Health check for the Selextract Worker API.',
|
|
232
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
233
|
+
},
|
|
234
|
+
// Tasks (AI-built)
|
|
235
|
+
{
|
|
236
|
+
name: 'task_create',
|
|
237
|
+
description: 'Create an AI-built task draft for a URL. Returns preview_id. This MCP server does not expose trace/scratchpad.',
|
|
238
|
+
inputSchema: {
|
|
239
|
+
type: 'object',
|
|
240
|
+
properties: {
|
|
241
|
+
url: { type: 'string', description: 'The page to analyze.' },
|
|
242
|
+
access_profile_id: { type: 'string', description: 'Optional access profile ID for logged-in/session scraping.' },
|
|
243
|
+
field_mode: { type: 'string', enum: ['auto', 'manual'], description: 'auto = infer fields; manual = use provided fields.' },
|
|
244
|
+
build_mode: { type: 'string', enum: ['selectors', 'flow', 'code', 'auto'], description: 'How to build the draft (default: auto).' },
|
|
245
|
+
script_source: { type: 'string', description: 'Optional Playwright script source (code mode). If provided, the server previews this exact script.' },
|
|
246
|
+
recipe_override: { type: 'object', description: 'Optional base recipe to reuse when previewing custom scripts (keeps flow/dom settings).' },
|
|
247
|
+
goal: { type: 'string', description: 'Optional short description of what to extract (helps in auto mode).' },
|
|
248
|
+
fields: { type: 'array', items: { type: 'object' }, description: 'Fields to extract (required in manual mode).' },
|
|
249
|
+
max_preview_rows: { type: 'number', description: 'How many sample rows to generate in the preview (default: 10).' },
|
|
250
|
+
advanced: { type: 'boolean', description: 'Allow more complex extraction strategies (may take longer).' },
|
|
251
|
+
},
|
|
252
|
+
required: ['url'],
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: 'task_build_status',
|
|
257
|
+
description: 'Get high-level build status for a task draft (queued/building/complete/failed). Returns only status + timing + error (no recipe, no notes).',
|
|
258
|
+
inputSchema: {
|
|
259
|
+
type: 'object',
|
|
260
|
+
properties: { preview_id: { type: 'string', description: 'The preview_id returned by task_create.' } },
|
|
261
|
+
required: ['preview_id'],
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
name: 'task_publish',
|
|
266
|
+
description: 'Publish a completed task draft as a saved task. Returns task_id. If you pass access_profile_id here, that access profile is attached to the saved task and used for runs.',
|
|
267
|
+
inputSchema: {
|
|
268
|
+
type: 'object',
|
|
269
|
+
properties: {
|
|
270
|
+
preview_id: { type: 'string', description: 'The preview_id to publish.' },
|
|
271
|
+
name: { type: 'string', description: 'Name for the saved task.' },
|
|
272
|
+
access_profile_id: { type: 'string', description: 'Optional access profile to attach to the saved task.' },
|
|
273
|
+
},
|
|
274
|
+
required: ['preview_id', 'name'],
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: 'task_draft_delete',
|
|
279
|
+
description: 'Delete a task draft by preview_id (frees a draft quota slot).',
|
|
280
|
+
inputSchema: {
|
|
281
|
+
type: 'object',
|
|
282
|
+
properties: { preview_id: { type: 'string', description: 'The preview_id to delete.' } },
|
|
283
|
+
required: ['preview_id'],
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: 'task_repair',
|
|
288
|
+
description: 'Repair a saved task by re-running the agent builder (creates a new recipe version and updates the task in place). Returns build_job_id.',
|
|
289
|
+
inputSchema: {
|
|
290
|
+
type: 'object',
|
|
291
|
+
properties: {
|
|
292
|
+
task_id: { type: 'string', description: 'Task ID.' },
|
|
293
|
+
hint: { type: 'string', description: 'Optional note about what is wrong (helps steer the repair).' },
|
|
294
|
+
force: { type: 'boolean', description: 'If true, queue even if a build is already in progress.' },
|
|
295
|
+
},
|
|
296
|
+
required: ['task_id'],
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: 'task_recipe_versions',
|
|
301
|
+
description: 'List recipe versions for a task (for rollback/version switching).',
|
|
302
|
+
inputSchema: {
|
|
303
|
+
type: 'object',
|
|
304
|
+
properties: { task_id: { type: 'string', description: 'Task ID.' } },
|
|
305
|
+
required: ['task_id'],
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: 'task_set_recipe_version',
|
|
310
|
+
description: 'Switch a task to use a specific recipe version (by recipe_version_id or by version number).',
|
|
311
|
+
inputSchema: {
|
|
312
|
+
type: 'object',
|
|
313
|
+
properties: {
|
|
314
|
+
task_id: { type: 'string', description: 'Task ID.' },
|
|
315
|
+
recipe_version_id: { type: 'string', description: 'Recipe version ID.' },
|
|
316
|
+
version: { type: 'number', description: 'Recipe version number (1, 2, 3, ...).' },
|
|
317
|
+
},
|
|
318
|
+
required: ['task_id'],
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
// Runs
|
|
322
|
+
{
|
|
323
|
+
name: 'run_create',
|
|
324
|
+
description: 'Create and enqueue a run for a saved task. Returns run_id. Runs use the access_profile_id attached to the task (if any).',
|
|
325
|
+
inputSchema: {
|
|
326
|
+
type: 'object',
|
|
327
|
+
properties: {
|
|
328
|
+
task_id: { type: 'string', description: 'Task ID.' },
|
|
329
|
+
max_runtime_seconds: { type: 'number', description: 'Optional hard limit for run time (seconds).' },
|
|
330
|
+
},
|
|
331
|
+
required: ['task_id'],
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: 'run_get',
|
|
336
|
+
description: 'Get run status/metadata by run_id.',
|
|
337
|
+
inputSchema: {
|
|
338
|
+
type: 'object',
|
|
339
|
+
properties: { run_id: { type: 'string', description: 'Run ID.' } },
|
|
340
|
+
required: ['run_id'],
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
name: 'run_list',
|
|
345
|
+
description: 'List recent runs for a task.',
|
|
346
|
+
inputSchema: {
|
|
347
|
+
type: 'object',
|
|
348
|
+
properties: {
|
|
349
|
+
task_id: { type: 'string', description: 'Task ID.' },
|
|
350
|
+
limit: { type: 'number', description: 'Max runs to return (default: 20).' },
|
|
351
|
+
},
|
|
352
|
+
required: ['task_id'],
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
name: 'run_stop',
|
|
357
|
+
description: 'Request a run stop (best-effort).',
|
|
358
|
+
inputSchema: {
|
|
359
|
+
type: 'object',
|
|
360
|
+
properties: { run_id: { type: 'string', description: 'Run ID.' } },
|
|
361
|
+
required: ['run_id'],
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: 'run_delete',
|
|
366
|
+
description: 'Soft-delete (hide) a finalized run.',
|
|
367
|
+
inputSchema: {
|
|
368
|
+
type: 'object',
|
|
369
|
+
properties: { run_id: { type: 'string', description: 'Run ID.' } },
|
|
370
|
+
required: ['run_id'],
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
name: 'run_results',
|
|
375
|
+
description: 'Fetch results for a completed run (paginated). Use cursor for the next page; use limit to control page size.',
|
|
376
|
+
inputSchema: {
|
|
377
|
+
type: 'object',
|
|
378
|
+
properties: {
|
|
379
|
+
run_id: { type: 'string', description: 'Run ID (must be completed).' },
|
|
380
|
+
limit: { type: 'number', description: 'Rows per page (1-1000, default: 100).' },
|
|
381
|
+
cursor: { type: 'string', description: 'Cursor from the previous page (recommended).' },
|
|
382
|
+
offset: { type: 'number', description: 'Optional offset-based paging (simple, but less stable than cursor).' },
|
|
383
|
+
},
|
|
384
|
+
required: ['run_id'],
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
// Access profiles
|
|
388
|
+
{
|
|
389
|
+
name: 'access_profile_list',
|
|
390
|
+
description: 'List access profiles (used for logged-in/session scraping).',
|
|
391
|
+
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
name: 'access_profile_get',
|
|
395
|
+
description: 'Fetch one access profile by access_profile_id.',
|
|
396
|
+
inputSchema: {
|
|
397
|
+
type: 'object',
|
|
398
|
+
properties: { access_profile_id: { type: 'string', description: 'Access profile ID.' } },
|
|
399
|
+
required: ['access_profile_id'],
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
name: 'access_profile_create',
|
|
404
|
+
description: 'Create an access profile. Secrets (headers/credentials/storage_state) are stored server-side; responses do not include secrets.',
|
|
405
|
+
inputSchema: {
|
|
406
|
+
type: 'object',
|
|
407
|
+
properties: {
|
|
408
|
+
name: { type: 'string', description: 'Access profile name.' },
|
|
409
|
+
url: { type: 'string', description: 'Optional URL this profile is for.' },
|
|
410
|
+
canonical_domain: { type: 'string', description: 'Optional canonical domain (advanced).' },
|
|
411
|
+
kind: { type: 'string', enum: ['custom', 'credentials', 'session'] },
|
|
412
|
+
headers: { type: 'array', items: { type: 'object' }, description: 'Optional extra headers (values stored server-side).' },
|
|
413
|
+
inputs: { type: 'array', items: { type: 'object' }, description: 'Optional credential inputs (values stored server-side).' },
|
|
414
|
+
storage_state: { type: 'object', description: 'Optional Playwright storage_state JSON (stored server-side).' },
|
|
415
|
+
login: { type: 'object', description: 'Optional login helper config.' },
|
|
416
|
+
},
|
|
417
|
+
required: ['name'],
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
name: 'access_profile_update',
|
|
422
|
+
description: 'Update an access profile (partial update).',
|
|
423
|
+
inputSchema: {
|
|
424
|
+
type: 'object',
|
|
425
|
+
properties: {
|
|
426
|
+
access_profile_id: { type: 'string', description: 'Access profile ID.' },
|
|
427
|
+
patch: { type: 'object', description: 'Partial update object.' },
|
|
428
|
+
},
|
|
429
|
+
required: ['access_profile_id'],
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
name: 'access_profile_delete',
|
|
434
|
+
description: 'Delete an access profile.',
|
|
435
|
+
inputSchema: {
|
|
436
|
+
type: 'object',
|
|
437
|
+
properties: { access_profile_id: { type: 'string', description: 'Access profile ID.' } },
|
|
438
|
+
required: ['access_profile_id'],
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
name: 'access_profile_build_session',
|
|
443
|
+
description: 'Enqueue an AI-assisted session build for an access profile (if enabled on the server).',
|
|
444
|
+
inputSchema: {
|
|
445
|
+
type: 'object',
|
|
446
|
+
properties: {
|
|
447
|
+
access_profile_id: { type: 'string', description: 'Access profile ID.' },
|
|
448
|
+
start_url: { type: 'string', description: 'Optional URL to start the login flow from.' },
|
|
449
|
+
hint: { type: 'string', description: 'Optional note to help the agent log in.' },
|
|
450
|
+
},
|
|
451
|
+
required: ['access_profile_id'],
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
];
|
|
455
|
+
}
|
|
456
|
+
export function toolHandlers(api, maxChars) {
|
|
457
|
+
const handlers = {
|
|
458
|
+
health: async () => {
|
|
459
|
+
const result = await api.request({
|
|
460
|
+
method: 'GET',
|
|
461
|
+
path: '/health',
|
|
462
|
+
});
|
|
463
|
+
return asTextResult(result, maxChars);
|
|
464
|
+
},
|
|
465
|
+
task_create: async (raw) => {
|
|
466
|
+
const input = ToolInputs.task_create.parse(raw);
|
|
467
|
+
const effectiveFieldMode = input.field_mode ?? (input.fields?.length ? 'manual' : 'auto');
|
|
468
|
+
const result = await api.request({
|
|
469
|
+
method: 'POST',
|
|
470
|
+
path: '/v1/agent/extractions/build',
|
|
471
|
+
body: {
|
|
472
|
+
url: input.url,
|
|
473
|
+
access_profile_id: input.access_profile_id,
|
|
474
|
+
field_mode: effectiveFieldMode,
|
|
475
|
+
build_mode: input.build_mode,
|
|
476
|
+
...(input.script_source ? { script_source: input.script_source } : {}),
|
|
477
|
+
...(input.recipe_override ? { recipe_override: input.recipe_override } : {}),
|
|
478
|
+
goal: input.goal,
|
|
479
|
+
fields: input.fields,
|
|
480
|
+
maxPreviewRows: input.max_preview_rows,
|
|
481
|
+
advanced: input.advanced,
|
|
482
|
+
debug: false,
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
const previewId = typeof result?.preview_id === 'string' ? String(result.preview_id) : null;
|
|
486
|
+
return asTextResult(previewId ? { preview_id: previewId } : result, maxChars);
|
|
487
|
+
},
|
|
488
|
+
task_build_status: async (raw) => {
|
|
489
|
+
const input = ToolInputs.task_build_status.parse(raw);
|
|
490
|
+
const result = await api.request({
|
|
491
|
+
method: 'GET',
|
|
492
|
+
path: `/v1/agent/extractions/preview/${input.preview_id}`,
|
|
493
|
+
});
|
|
494
|
+
const safe = {
|
|
495
|
+
preview_id: result?.preview_id ?? input.preview_id,
|
|
496
|
+
status: result?.status ?? null,
|
|
497
|
+
error: result?.error ?? null,
|
|
498
|
+
queued_at: result?.queued_at ?? null,
|
|
499
|
+
started_at: result?.started_at ?? null,
|
|
500
|
+
finished_at: result?.finished_at ?? null,
|
|
501
|
+
duration_ms: result?.duration_ms ?? null,
|
|
502
|
+
};
|
|
503
|
+
return asTextResult(safe, maxChars);
|
|
504
|
+
},
|
|
505
|
+
task_publish: async (raw) => {
|
|
506
|
+
const input = ToolInputs.task_publish.parse(raw);
|
|
507
|
+
const result = await api.request({
|
|
508
|
+
method: 'POST',
|
|
509
|
+
path: `/v1/agent/drafts/${input.preview_id}/promote`,
|
|
510
|
+
body: {
|
|
511
|
+
name: input.name,
|
|
512
|
+
access_profile_id: input.access_profile_id,
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
const taskId = typeof result?.task_id === 'string' ? String(result.task_id) : null;
|
|
516
|
+
return asTextResult(taskId ? { task_id: taskId } : result, maxChars);
|
|
517
|
+
},
|
|
518
|
+
task_draft_delete: async (raw) => {
|
|
519
|
+
const input = ToolInputs.task_draft_delete.parse(raw);
|
|
520
|
+
const result = await api.request({
|
|
521
|
+
method: 'DELETE',
|
|
522
|
+
path: `/v1/agent/drafts/${input.preview_id}`,
|
|
523
|
+
});
|
|
524
|
+
return asTextResult(result, maxChars);
|
|
525
|
+
},
|
|
526
|
+
task_repair: async (raw) => {
|
|
527
|
+
const input = ToolInputs.task_repair.parse(raw);
|
|
528
|
+
const result = await api.request({
|
|
529
|
+
method: 'POST',
|
|
530
|
+
path: `/v1/agent/tasks/${input.task_id}/repair`,
|
|
531
|
+
body: input.hint || input.force ? { hint: input.hint, force: input.force } : {},
|
|
532
|
+
});
|
|
533
|
+
return asTextResult(result, maxChars);
|
|
534
|
+
},
|
|
535
|
+
task_recipe_versions: async (raw) => {
|
|
536
|
+
const input = ToolInputs.task_recipe_versions.parse(raw);
|
|
537
|
+
const result = await api.request({
|
|
538
|
+
method: 'GET',
|
|
539
|
+
path: `/v1/tasks/${input.task_id}/recipe-versions`,
|
|
540
|
+
});
|
|
541
|
+
return asTextResult(result, maxChars);
|
|
542
|
+
},
|
|
543
|
+
task_set_recipe_version: async (raw) => {
|
|
544
|
+
const input = ToolInputs.task_set_recipe_version.parse(raw);
|
|
545
|
+
const result = await api.request({
|
|
546
|
+
method: 'POST',
|
|
547
|
+
path: `/v1/tasks/${input.task_id}/recipe-version`,
|
|
548
|
+
body: {
|
|
549
|
+
recipe_version_id: input.recipe_version_id,
|
|
550
|
+
version: input.version,
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
return asTextResult(result, maxChars);
|
|
554
|
+
},
|
|
555
|
+
run_create: async (raw) => {
|
|
556
|
+
const input = ToolInputs.run_create.parse(raw);
|
|
557
|
+
const result = await api.request({
|
|
558
|
+
method: 'POST',
|
|
559
|
+
path: `/v1/tasks/${input.task_id}/runs`,
|
|
560
|
+
body: input.max_runtime_seconds ? { max_runtime_seconds: input.max_runtime_seconds } : {},
|
|
561
|
+
});
|
|
562
|
+
return asTextResult(result, maxChars);
|
|
563
|
+
},
|
|
564
|
+
run_get: async (raw) => {
|
|
565
|
+
const input = ToolInputs.run_get.parse(raw);
|
|
566
|
+
const result = await api.request({
|
|
567
|
+
method: 'GET',
|
|
568
|
+
path: `/v1/task-runs/${input.run_id}`,
|
|
569
|
+
});
|
|
570
|
+
return asTextResult(result, maxChars);
|
|
571
|
+
},
|
|
572
|
+
run_list: async (raw) => {
|
|
573
|
+
const input = ToolInputs.run_list.parse(raw);
|
|
574
|
+
const result = await api.request({
|
|
575
|
+
method: 'GET',
|
|
576
|
+
path: `/v1/tasks/${input.task_id}/runs`,
|
|
577
|
+
query: { limit: input.limit },
|
|
578
|
+
});
|
|
579
|
+
return asTextResult(result, maxChars);
|
|
580
|
+
},
|
|
581
|
+
run_stop: async (raw) => {
|
|
582
|
+
const input = ToolInputs.run_stop.parse(raw);
|
|
583
|
+
const result = await api.request({
|
|
584
|
+
method: 'POST',
|
|
585
|
+
path: `/v1/task-runs/${input.run_id}/stop`,
|
|
586
|
+
});
|
|
587
|
+
return asTextResult(result, maxChars);
|
|
588
|
+
},
|
|
589
|
+
run_delete: async (raw) => {
|
|
590
|
+
const input = ToolInputs.run_delete.parse(raw);
|
|
591
|
+
const result = await api.request({
|
|
592
|
+
method: 'DELETE',
|
|
593
|
+
path: `/v1/task-runs/${input.run_id}`,
|
|
594
|
+
});
|
|
595
|
+
return asTextResult(result, maxChars);
|
|
596
|
+
},
|
|
597
|
+
run_results: async (raw) => {
|
|
598
|
+
const input = ToolInputs.run_results.parse(raw);
|
|
599
|
+
const result = await api.request({
|
|
600
|
+
method: 'GET',
|
|
601
|
+
path: '/results/data',
|
|
602
|
+
query: {
|
|
603
|
+
run_id: input.run_id,
|
|
604
|
+
limit: input.limit,
|
|
605
|
+
cursor: input.cursor,
|
|
606
|
+
offset: input.offset,
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
return asTextResult(result, maxChars);
|
|
610
|
+
},
|
|
611
|
+
access_profile_list: async () => {
|
|
612
|
+
const result = await api.request({
|
|
613
|
+
method: 'GET',
|
|
614
|
+
path: '/v1/access-profiles',
|
|
615
|
+
});
|
|
616
|
+
return asTextResult(result, maxChars);
|
|
617
|
+
},
|
|
618
|
+
access_profile_get: async (raw) => {
|
|
619
|
+
const input = ToolInputs.access_profile_get.parse(raw);
|
|
620
|
+
const result = await api.request({
|
|
621
|
+
method: 'GET',
|
|
622
|
+
path: `/v1/access-profiles/${input.access_profile_id}`,
|
|
623
|
+
});
|
|
624
|
+
return asTextResult(result, maxChars);
|
|
625
|
+
},
|
|
626
|
+
access_profile_create: async (raw) => {
|
|
627
|
+
const input = ToolInputs.access_profile_create.parse(raw);
|
|
628
|
+
const result = await api.request({
|
|
629
|
+
method: 'POST',
|
|
630
|
+
path: '/v1/access-profiles',
|
|
631
|
+
body: input,
|
|
632
|
+
});
|
|
633
|
+
return asTextResult(result, maxChars);
|
|
634
|
+
},
|
|
635
|
+
access_profile_update: async (raw) => {
|
|
636
|
+
const input = ToolInputs.access_profile_update.parse(raw);
|
|
637
|
+
const result = await api.request({
|
|
638
|
+
method: 'PATCH',
|
|
639
|
+
path: `/v1/access-profiles/${input.access_profile_id}`,
|
|
640
|
+
body: input.patch,
|
|
641
|
+
});
|
|
642
|
+
return asTextResult(result, maxChars);
|
|
643
|
+
},
|
|
644
|
+
access_profile_delete: async (raw) => {
|
|
645
|
+
const input = ToolInputs.access_profile_delete.parse(raw);
|
|
646
|
+
const result = await api.request({
|
|
647
|
+
method: 'DELETE',
|
|
648
|
+
path: `/v1/access-profiles/${input.access_profile_id}`,
|
|
649
|
+
});
|
|
650
|
+
return asTextResult(result, maxChars);
|
|
651
|
+
},
|
|
652
|
+
access_profile_build_session: async (raw) => {
|
|
653
|
+
const input = ToolInputs.access_profile_build_session.parse(raw);
|
|
654
|
+
const body = input.start_url || input.hint ? { start_url: input.start_url, hint: input.hint } : undefined;
|
|
655
|
+
const result = await api.request({
|
|
656
|
+
method: 'POST',
|
|
657
|
+
path: `/v1/access-profiles/${input.access_profile_id}/build-session`,
|
|
658
|
+
body,
|
|
659
|
+
});
|
|
660
|
+
return asTextResult(result, maxChars);
|
|
661
|
+
},
|
|
662
|
+
};
|
|
663
|
+
return handlers;
|
|
664
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@selextract/mcp-selextract",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Selextract Cloud MCP server (local stdio) for calling the Selextract Worker API",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"keywords": [
|
|
12
|
+
"mcp",
|
|
13
|
+
"model-context-protocol",
|
|
14
|
+
"selextract",
|
|
15
|
+
"scraping",
|
|
16
|
+
"web-scraping"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"selextract-mcp": "dist/cli.js",
|
|
23
|
+
"mcp-selextract": "dist/cli.js"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"dev": "tsx src/cli.ts",
|
|
27
|
+
"build": "rm -rf dist && tsc",
|
|
28
|
+
"prepublishOnly": "npm run build",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"type-check": "tsc --noEmit"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
34
|
+
"dotenv": "^16.3.1",
|
|
35
|
+
"zod": "^3.22.4"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.8.0",
|
|
39
|
+
"tsx": "^3.14.0",
|
|
40
|
+
"typescript": "^5.2.0"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|