@webstew/agent-tools 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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env sh
2
+ exec node "$(dirname "$0")/../dist/index.js" "$@"
package/dist/api.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { BridgeAuth } from './auth';
2
+ export declare class WebstewApiError extends Error {
3
+ status: number;
4
+ constructor(status: number, message: string);
5
+ }
6
+ export declare function callWebstew<T = unknown>(auth: BridgeAuth, method: 'GET' | 'POST' | 'DELETE', pathStr: string, body?: unknown, query?: Record<string, string>): Promise<T>;
package/dist/api.js ADDED
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ // Thin HTTP client to Webstew's /api/mcp/* endpoints. All requests use
3
+ // the bridge's pairing token (Bearer auth) so the server can resolve
4
+ // the user. No state — just a callable.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.WebstewApiError = void 0;
7
+ exports.callWebstew = callWebstew;
8
+ class WebstewApiError extends Error {
9
+ status;
10
+ constructor(status, message) {
11
+ super(message);
12
+ this.status = status;
13
+ }
14
+ }
15
+ exports.WebstewApiError = WebstewApiError;
16
+ async function callWebstew(auth, method, pathStr, body, query) {
17
+ const url = new URL(auth.serverUrl.replace(/\/$/, '') + pathStr);
18
+ if (query) {
19
+ for (const [k, v] of Object.entries(query))
20
+ url.searchParams.set(k, v);
21
+ }
22
+ const res = await fetch(url, {
23
+ method,
24
+ headers: {
25
+ Authorization: `Bearer ${auth.pairingToken}`,
26
+ ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
27
+ },
28
+ body: body === undefined ? undefined : JSON.stringify(body),
29
+ });
30
+ if (!res.ok) {
31
+ let detail = '';
32
+ try {
33
+ const j = (await res.json());
34
+ detail = j?.error || '';
35
+ }
36
+ catch { }
37
+ throw new WebstewApiError(res.status, detail || `${method} ${pathStr} → HTTP ${res.status}`);
38
+ }
39
+ // Some endpoints return text or empty; handle JSON-or-not gracefully.
40
+ const ct = res.headers.get('content-type') || '';
41
+ if (ct.includes('application/json')) {
42
+ return (await res.json());
43
+ }
44
+ return (await res.text());
45
+ }
package/dist/auth.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export interface BridgeAuth {
2
+ serverUrl: string;
3
+ pairingToken: string;
4
+ bridgeId: string;
5
+ }
6
+ export declare function loadAuthOrThrow(): BridgeAuth;
package/dist/auth.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ // Read the bridge's stored pairing token + server URL. Same file
3
+ // @webstew/bridge writes — we just consume it from a different process.
4
+ // If the bridge isn't paired, the MCP server can't talk to Webstew, so
5
+ // we fail fast with a clear message claude can surface to the user.
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.loadAuthOrThrow = loadAuthOrThrow;
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const node_os_1 = __importDefault(require("node:os"));
14
+ function loadAuthOrThrow() {
15
+ const p = node_path_1.default.join(node_os_1.default.homedir(), '.webstew', 'bridge.json');
16
+ if (!node_fs_1.default.existsSync(p)) {
17
+ throw new Error('Webstew bridge not paired. Run `webstew-bridge connect <code>` first ' +
18
+ '(get a code from https://webstew.net/integrations).');
19
+ }
20
+ const raw = node_fs_1.default.readFileSync(p, 'utf8');
21
+ const parsed = JSON.parse(raw);
22
+ if (!parsed.serverUrl || !parsed.pairingToken) {
23
+ throw new Error('Webstew bridge config at ~/.webstew/bridge.json is incomplete.');
24
+ }
25
+ return {
26
+ serverUrl: parsed.serverUrl,
27
+ pairingToken: parsed.pairingToken,
28
+ bridgeId: parsed.bridgeId || '',
29
+ };
30
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env -S npx --yes tsx
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // @webstew/agent-tools — MCP server entry. Spawned as a subprocess by
4
+ // Claude Code via `--mcp-config`. Communicates over stdio (JSON-RPC).
5
+ //
6
+ // Tool surface: webstew_* tools that proxy to Webstew's /api/mcp/*
7
+ // endpoints. Auth is the bridge's pairing token at ~/.webstew/bridge.json
8
+ // (so installing this package alone is useless — it relies on the
9
+ // bridge being paired first).
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ const mcp_1 = require("./mcp");
12
+ const auth_1 = require("./auth");
13
+ const cms_1 = require("./tools/cms");
14
+ const media_1 = require("./tools/media");
15
+ const grader_1 = require("./tools/grader");
16
+ const workspace_1 = require("./tools/workspace");
17
+ const integrations_1 = require("./tools/integrations");
18
+ const VERSION = (() => {
19
+ try {
20
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
21
+ return require('../package.json').version;
22
+ }
23
+ catch {
24
+ return '0.0.0-dev';
25
+ }
26
+ })();
27
+ function main() {
28
+ // Resolve auth FIRST. If the bridge isn't paired, fail with a clear
29
+ // message claude can surface — better than tools that error per-call.
30
+ let auth;
31
+ try {
32
+ auth = (0, auth_1.loadAuthOrThrow)();
33
+ }
34
+ catch (e) {
35
+ process.stderr.write(`[webstew-agent-tools] ${e?.message || e}\n`);
36
+ process.exit(1);
37
+ }
38
+ const tools = [
39
+ ...(0, workspace_1.workspaceTools)(auth),
40
+ ...(0, cms_1.cmsTools)(auth),
41
+ ...(0, media_1.mediaTools)(auth),
42
+ ...(0, grader_1.graderTools)(auth),
43
+ ...(0, integrations_1.integrationTools)(auth),
44
+ ];
45
+ (0, mcp_1.startMcpServer)({
46
+ name: 'webstew-agent-tools',
47
+ version: VERSION,
48
+ tools,
49
+ });
50
+ }
51
+ main();
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ export interface McpTool {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: 'object';
6
+ properties: Record<string, any>;
7
+ required?: string[];
8
+ };
9
+ /** Returns either plain text (will be wrapped) or a full content array. */
10
+ handler: (args: Record<string, any>) => Promise<string | McpContentBlock[]>;
11
+ }
12
+ export type McpContentBlock = {
13
+ type: 'text';
14
+ text: string;
15
+ } | {
16
+ type: 'image';
17
+ data: string;
18
+ mimeType: string;
19
+ };
20
+ export declare function startMcpServer(opts: {
21
+ name: string;
22
+ version: string;
23
+ tools: McpTool[];
24
+ }): void;
package/dist/mcp.js ADDED
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ // Minimal MCP (Model Context Protocol) server implementation. JSON-RPC
3
+ // 2.0 over stdio, just the methods Claude Code calls:
4
+ // • initialize — handshake (return our serverInfo + capabilities)
5
+ // • tools/list — return our tool registry
6
+ // • tools/call — invoke a tool with arguments, return content
7
+ // • notifications/* — fire-and-forget; we ignore them
8
+ // • shutdown / exit — exit gracefully
9
+ //
10
+ // We hand-roll instead of pulling @modelcontextprotocol/sdk to keep the
11
+ // MCP server bundle tiny + dep-free. Spec we implement:
12
+ // https://spec.modelcontextprotocol.io/specification/2024-11-05/
13
+ var __importDefault = (this && this.__importDefault) || function (mod) {
14
+ return (mod && mod.__esModule) ? mod : { "default": mod };
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.startMcpServer = startMcpServer;
18
+ const node_readline_1 = __importDefault(require("node:readline"));
19
+ const PROTOCOL_VERSION = '2024-11-05';
20
+ function startMcpServer(opts) {
21
+ const send = (msg) => {
22
+ process.stdout.write(JSON.stringify(msg) + '\n');
23
+ };
24
+ const error = (id, code, message) => send({ jsonrpc: '2.0', id, error: { code, message } });
25
+ const result = (id, value) => send({ jsonrpc: '2.0', id, result: value });
26
+ const toolsByName = new Map(opts.tools.map((t) => [t.name, t]));
27
+ const rl = node_readline_1.default.createInterface({ input: process.stdin });
28
+ rl.on('line', async (line) => {
29
+ const trimmed = line.trim();
30
+ if (!trimmed)
31
+ return;
32
+ let req;
33
+ try {
34
+ req = JSON.parse(trimmed);
35
+ }
36
+ catch {
37
+ // Parse error — per JSON-RPC spec, id is unknown so use null.
38
+ error(null, -32700, 'Parse error');
39
+ return;
40
+ }
41
+ const id = req.id;
42
+ // Notifications (no id) — ack silently, never respond.
43
+ const isNotification = id === undefined || id === null;
44
+ const respond = (value) => { if (!isNotification)
45
+ result(id, value); };
46
+ const respondError = (code, message) => {
47
+ if (!isNotification)
48
+ error(id, code, message);
49
+ };
50
+ switch (req.method) {
51
+ case 'initialize': {
52
+ respond({
53
+ protocolVersion: PROTOCOL_VERSION,
54
+ capabilities: { tools: {} },
55
+ serverInfo: { name: opts.name, version: opts.version },
56
+ });
57
+ return;
58
+ }
59
+ case 'tools/list': {
60
+ respond({
61
+ tools: opts.tools.map((t) => ({
62
+ name: t.name,
63
+ description: t.description,
64
+ inputSchema: t.inputSchema,
65
+ })),
66
+ });
67
+ return;
68
+ }
69
+ case 'tools/call': {
70
+ const name = req.params?.name;
71
+ const args = (req.params?.arguments || {});
72
+ const tool = toolsByName.get(name);
73
+ if (!tool) {
74
+ respondError(-32601, `Unknown tool: ${name}`);
75
+ return;
76
+ }
77
+ try {
78
+ const out = await tool.handler(args);
79
+ const content = typeof out === 'string'
80
+ ? [{ type: 'text', text: out }]
81
+ : out;
82
+ respond({ content, isError: false });
83
+ }
84
+ catch (e) {
85
+ respond({
86
+ content: [{ type: 'text', text: e?.message || String(e) }],
87
+ isError: true,
88
+ });
89
+ }
90
+ return;
91
+ }
92
+ case 'shutdown':
93
+ case 'exit': {
94
+ respond({});
95
+ process.exit(0);
96
+ return;
97
+ }
98
+ case 'ping': {
99
+ respond({});
100
+ return;
101
+ }
102
+ default: {
103
+ // Unknown method — return -32601 only if it's a real request.
104
+ if (req.method.startsWith('notifications/'))
105
+ return;
106
+ respondError(-32601, `Method not found: ${req.method}`);
107
+ return;
108
+ }
109
+ }
110
+ });
111
+ // Clean exit when stdin closes (claude shuts us down by closing it).
112
+ rl.on('close', () => process.exit(0));
113
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpTool } from '../mcp';
2
+ import type { BridgeAuth } from '../auth';
3
+ export declare function cmsTools(auth: BridgeAuth): McpTool[];
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ // CMS tools — read + write the project's content collections.
3
+ // Mirror the in-app agent's cms_* tools (see apps/web/src/lib/agent-tools.ts).
4
+ // All operations require the user to be on a saved project (projectId
5
+ // flows through the server's session, so the MCP server doesn't have
6
+ // to know about it).
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.cmsTools = cmsTools;
9
+ const api_1 = require("../api");
10
+ function cmsTools(auth) {
11
+ return [
12
+ {
13
+ name: 'webstew_list_cms_collections',
14
+ description: 'List the content collections in the active Webstew project. ' +
15
+ 'Each collection has a slug, name, item count, and field schema. ' +
16
+ 'Use this BEFORE list_cms_items so you know what collections exist. ' +
17
+ 'Returns empty list when the user has no saved project yet.',
18
+ inputSchema: { type: 'object', properties: {} },
19
+ handler: async () => {
20
+ const out = await (0, api_1.callWebstew)(auth, 'GET', '/api/mcp/cms/collections');
21
+ return JSON.stringify(out.collections, null, 2);
22
+ },
23
+ },
24
+ {
25
+ name: 'webstew_list_cms_items',
26
+ description: 'List items in a specific content collection. Provide the collection slug ' +
27
+ '(e.g. "blog-posts", "services"). Returns up to 50 items with their slug, ' +
28
+ 'status (draft/published), and fields.',
29
+ inputSchema: {
30
+ type: 'object',
31
+ properties: {
32
+ collection: { type: 'string', description: 'Collection slug' },
33
+ },
34
+ required: ['collection'],
35
+ },
36
+ handler: async (args) => {
37
+ const out = await (0, api_1.callWebstew)(auth, 'GET', '/api/mcp/cms/items', undefined, { collection: String(args.collection) });
38
+ return JSON.stringify(out.items, null, 2);
39
+ },
40
+ },
41
+ {
42
+ name: 'webstew_create_cms_item',
43
+ description: 'Create a new item in a collection. Field names must match the collection ' +
44
+ 'schema (use list_cms_collections first). Default status is "published" — ' +
45
+ 'pass status: "draft" if the user wants to stage it.',
46
+ inputSchema: {
47
+ type: 'object',
48
+ properties: {
49
+ collection: { type: 'string', description: 'Collection slug' },
50
+ slug: { type: 'string', description: 'URL-safe slug for the item' },
51
+ fields: { type: 'object', description: 'Field values matching the collection schema' },
52
+ status: { type: 'string', enum: ['draft', 'published'], description: 'Defaults to published' },
53
+ },
54
+ required: ['collection', 'slug', 'fields'],
55
+ },
56
+ handler: async (args) => {
57
+ const out = await (0, api_1.callWebstew)(auth, 'POST', '/api/mcp/cms/items', {
58
+ collection: args.collection,
59
+ slug: args.slug,
60
+ fields: args.fields,
61
+ status: args.status || 'published',
62
+ });
63
+ return out.ok
64
+ ? `Created item ${args.slug} in ${args.collection} (id: ${out.itemId})`
65
+ : `Failed to create item.`;
66
+ },
67
+ },
68
+ ];
69
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpTool } from '../mcp';
2
+ import type { BridgeAuth } from '../auth';
3
+ export declare function graderTools(auth: BridgeAuth): McpTool[];
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ // Grader tool — runs Webstew's SEO + AI-visibility scoring against a
3
+ // URL. Returns overall score (0-100), category breakdowns, and a list
4
+ // of actionable issues with severity. Use when the user asks "how's
5
+ // my site doing?", "what's missing?", or before/after a major refactor.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.graderTools = graderTools;
8
+ const api_1 = require("../api");
9
+ function graderTools(auth) {
10
+ return [
11
+ {
12
+ name: 'webstew_grade_site',
13
+ description: 'Grade a public URL for SEO + technical health + AI-visibility (how easily ' +
14
+ 'LLM-powered search like Perplexity / ChatGPT-search can answer questions about ' +
15
+ 'the site). Returns scores 0-100 plus an itemized issue list. After grading, ' +
16
+ 'fix the top 2-3 actionable issues yourself (add missing meta description, fix ' +
17
+ 'heading structure, add schema.org JSON-LD) instead of pasting the report back.',
18
+ inputSchema: {
19
+ type: 'object',
20
+ properties: {
21
+ url: { type: 'string', description: 'Public URL to grade. Required to be reachable.' },
22
+ },
23
+ required: ['url'],
24
+ },
25
+ handler: async (args) => {
26
+ const out = await (0, api_1.callWebstew)(auth, 'POST', '/api/mcp/grader', { url: args.url });
27
+ return JSON.stringify(out, null, 2);
28
+ },
29
+ },
30
+ ];
31
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpTool } from '../mcp';
2
+ import type { BridgeAuth } from '../auth';
3
+ export declare function integrationTools(auth: BridgeAuth): McpTool[];
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ // Composio integrations — the user's connected accounts (Gmail, Slack,
3
+ // HubSpot, Stripe, Shopify, GitHub, etc.). Three-step flow:
4
+ // 1. webstew_list_integrations — what's connected
5
+ // 2. webstew_list_integration_actions — what verbs the connected
6
+ // toolkit exposes (e.g. for
7
+ // Gmail: SEND_EMAIL, etc.)
8
+ // 3. webstew_run_integration_action — actually do the thing
9
+ //
10
+ // NEVER guess action slugs — always confirm with list_integration_actions.
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.integrationTools = integrationTools;
13
+ const api_1 = require("../api");
14
+ function integrationTools(auth) {
15
+ return [
16
+ {
17
+ name: 'webstew_list_integrations',
18
+ description: 'List the user\'s connected third-party integrations (Gmail, Slack, HubSpot, ' +
19
+ 'Stripe, Shopify, GitHub, Salesforce, Google Sheets, etc.). Returns the toolkit ' +
20
+ 'slug, label, and connection status. If a toolkit the user asked for isn\'t in ' +
21
+ 'the result, tell them to connect it at /integrations and stop.',
22
+ inputSchema: { type: 'object', properties: {} },
23
+ handler: async () => {
24
+ const out = await (0, api_1.callWebstew)(auth, 'GET', '/api/mcp/integrations');
25
+ return JSON.stringify(out.integrations, null, 2);
26
+ },
27
+ },
28
+ {
29
+ name: 'webstew_list_integration_actions',
30
+ description: 'List the available actions/verbs for a connected toolkit. Pass the toolkit slug ' +
31
+ '(e.g. "gmail", "slack", "hubspot"). Returns action slugs you pass to run_integration_action. ' +
32
+ 'Use this BEFORE run_integration_action — never guess action slugs.',
33
+ inputSchema: {
34
+ type: 'object',
35
+ properties: {
36
+ toolkit: { type: 'string', description: 'Toolkit slug, e.g. "gmail" or "slack"' },
37
+ },
38
+ required: ['toolkit'],
39
+ },
40
+ handler: async (args) => {
41
+ const out = await (0, api_1.callWebstew)(auth, 'GET', '/api/mcp/integrations/actions', undefined, { toolkit: String(args.toolkit) });
42
+ return JSON.stringify(out.actions, null, 2);
43
+ },
44
+ },
45
+ {
46
+ name: 'webstew_run_integration_action',
47
+ description: 'Execute one action against a connected integration. action must be a slug returned ' +
48
+ 'by list_integration_actions for the relevant toolkit. args shape depends on the action ' +
49
+ '— inspect the action description first. Examples: send a Slack message, create a ' +
50
+ 'HubSpot contact, append a Google Sheets row, send a Gmail email.',
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: {
54
+ action: { type: 'string', description: 'Action slug, e.g. "GMAIL_SEND_EMAIL"' },
55
+ args: { type: 'object', description: 'Action arguments — shape per the action schema' },
56
+ },
57
+ required: ['action', 'args'],
58
+ },
59
+ handler: async (args) => {
60
+ const out = await (0, api_1.callWebstew)(auth, 'POST', '/api/mcp/integrations/run', { action: args.action, args: args.args });
61
+ if (!out.ok)
62
+ return `Action failed: ${out.error || 'unknown error'}`;
63
+ return typeof out.data === 'string'
64
+ ? out.data
65
+ : JSON.stringify(out.data, null, 2);
66
+ },
67
+ },
68
+ ];
69
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpTool } from '../mcp';
2
+ import type { BridgeAuth } from '../auth';
3
+ export declare function mediaTools(auth: BridgeAuth): McpTool[];
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ // Media tools — upload the user's own image to Cloudinary (vs the
3
+ // /api/media Pexels proxy which is built into URLs the chef writes).
4
+ // Use this when the user supplies an external image URL they want
5
+ // stored permanently in their project.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.mediaTools = mediaTools;
8
+ const api_1 = require("../api");
9
+ function mediaTools(auth) {
10
+ return [
11
+ {
12
+ name: 'webstew_upload_image',
13
+ description: 'Upload an image to the user\'s Cloudinary store. Use when the user provides ' +
14
+ 'their own image URL they want stored permanently. Skip for /api/media (Pexels proxy), ' +
15
+ 'pravatar avatars, or URLs already on cloudinary.com — those are already proxied or ' +
16
+ 'stable. Returns the permanent Cloudinary URL.',
17
+ inputSchema: {
18
+ type: 'object',
19
+ properties: {
20
+ sourceUrl: { type: 'string', description: 'URL of the image to upload + store' },
21
+ },
22
+ required: ['sourceUrl'],
23
+ },
24
+ handler: async (args) => {
25
+ const out = await (0, api_1.callWebstew)(auth, 'POST', '/api/mcp/media/upload', { sourceUrl: args.sourceUrl });
26
+ return out.url;
27
+ },
28
+ },
29
+ ];
30
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpTool } from '../mcp';
2
+ import type { BridgeAuth } from '../auth';
3
+ export declare function workspaceTools(auth: BridgeAuth): McpTool[];
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ // Workspace-control tools — change the user's workspace settings from
3
+ // chat. Currently just switch_target. Future: change project name,
4
+ // add/remove pages, etc.
5
+ //
6
+ // IMPORTANT: switch_target requires user approval. The MCP server
7
+ // POSTs the request to /api/mcp/workspace/switch-target which emits
8
+ // a permission event on the active agent SSE stream. The workspace UI
9
+ // renders an inline "Approve / Deny" prompt. The HTTP call BLOCKS
10
+ // here until the user decides (or 60s timeout). So chef calling this
11
+ // tool synchronously waits for the user to click. Good UX: chef
12
+ // proposes, user confirms, target switches, next turn happens in the
13
+ // new context.
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.workspaceTools = workspaceTools;
16
+ const api_1 = require("../api");
17
+ const VALID_TARGETS = ['website', 'nextjs', 'react', 'astro', 'expo'];
18
+ // Sidebar panels the workspace exposes — chef can request the user
19
+ // switch to one. Names match the workspace's `Panel` union exactly.
20
+ const VALID_PANELS = [
21
+ 'build', // chat + AI build
22
+ 'templates', // template gallery
23
+ 'projects', // saved projects / Files
24
+ 'images', // image library
25
+ 'video', // video generation
26
+ 'integrations', // APIs / Composio + bridge
27
+ 'env', // env variables
28
+ 'console', // logs
29
+ 'deploy', // deployment + custom domains
30
+ 'webstew', // Stew (chef tools / AI features)
31
+ ];
32
+ function workspaceTools(auth) {
33
+ return [
34
+ {
35
+ name: 'webstew_open_panel',
36
+ description: 'Request that the workspace open a specific sidebar panel. Use when the user\'s ' +
37
+ 'task is better done in a panel than in chat (e.g. "open the deploy panel" to ' +
38
+ 'show their deploy options, "open integrations" to connect Gmail). User sees an ' +
39
+ 'Approve / Deny prompt. Valid panels: ' + VALID_PANELS.join(', ') + '.',
40
+ inputSchema: {
41
+ type: 'object',
42
+ properties: {
43
+ panel: {
44
+ type: 'string',
45
+ enum: [...VALID_PANELS],
46
+ description: 'Panel id to open',
47
+ },
48
+ reason: { type: 'string', description: 'One-sentence reason shown to the user' },
49
+ },
50
+ required: ['panel', 'reason'],
51
+ },
52
+ handler: async (args) => {
53
+ const panel = String(args.panel);
54
+ if (!VALID_PANELS.includes(panel)) {
55
+ return `Invalid panel "${panel}". Valid: ${VALID_PANELS.join(', ')}.`;
56
+ }
57
+ const out = await (0, api_1.callWebstew)(auth, 'POST', '/api/mcp/workspace/open-panel', { panel, reason: String(args.reason || '') });
58
+ if (out.timedOut)
59
+ return 'Timed out waiting for user — ask them again if needed.';
60
+ return out.approved
61
+ ? `User approved — workspace switched to the ${panel} panel.`
62
+ : `User declined opening the ${panel} panel. Continuing in chat.`;
63
+ },
64
+ },
65
+ {
66
+ name: 'webstew_switch_target',
67
+ description: 'Request that the workspace switch to a different build target. The user will see ' +
68
+ 'an Approve/Deny prompt and the call returns their decision. Valid targets: ' +
69
+ 'website, nextjs, react, astro, expo. Use this when the user\'s request implies a ' +
70
+ 'different runtime than the current workspace (e.g. asks for a mobile app while on ' +
71
+ 'website). After the user approves, the workspace switches and your NEXT turn ' +
72
+ 'will be in the new target — don\'t try to scaffold the new project in this turn.',
73
+ inputSchema: {
74
+ type: 'object',
75
+ properties: {
76
+ target: {
77
+ type: 'string',
78
+ enum: [...VALID_TARGETS],
79
+ description: 'The target to switch to',
80
+ },
81
+ reason: {
82
+ type: 'string',
83
+ description: 'One-sentence reason shown to the user in the approval prompt',
84
+ },
85
+ },
86
+ required: ['target', 'reason'],
87
+ },
88
+ handler: async (args) => {
89
+ const target = String(args.target);
90
+ if (!VALID_TARGETS.includes(target)) {
91
+ return `Invalid target "${target}". Valid: ${VALID_TARGETS.join(', ')}.`;
92
+ }
93
+ try {
94
+ const out = await (0, api_1.callWebstew)(auth, 'POST', '/api/mcp/workspace/switch-target', { target, reason: String(args.reason || '') });
95
+ if (out.approved) {
96
+ return `Switched to ${out.target || target}. The user is now in the new workspace — their next message will operate there.`;
97
+ }
98
+ return `User declined the switch. Continuing in the current target.`;
99
+ }
100
+ catch (e) {
101
+ if (e instanceof api_1.WebstewApiError && e.status === 408) {
102
+ return 'Timed out waiting for user approval. Ask them in chat to try again.';
103
+ }
104
+ throw e;
105
+ }
106
+ },
107
+ },
108
+ ];
109
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@webstew/agent-tools",
3
+ "version": "0.1.0",
4
+ "description": "MCP server exposing Webstew CMS, integrations, image, grader, and workspace tools to Claude Code via the local bridge.",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "bin": {
8
+ "agent-tools": "./bin/agent-tools"
9
+ },
10
+ "files": ["dist/", "bin/", "README.md"],
11
+ "scripts": {
12
+ "build": "tsc && node -e \"const fs=require('fs'),f='dist/index.js';let c=fs.readFileSync(f,'utf8');c=c.replace(/^#!.*\\n/,'');fs.writeFileSync(f,'#!/usr/bin/env node\\n'+c);fs.chmodSync(f,0o755)\"",
13
+ "prepack": "npm run build",
14
+ "dev": "tsx src/index.ts"
15
+ },
16
+ "dependencies": {
17
+ "tsx": "^4.7.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^20.11.16",
21
+ "typescript": "^5.3.3"
22
+ },
23
+ "engines": { "node": ">=18" },
24
+ "repository": { "type": "git", "url": "https://github.com/SGK112/ai-website-builder" },
25
+ "keywords": ["webstew", "mcp", "claude", "ai", "tools"],
26
+ "license": "MIT"
27
+ }