@tapicker/bridge 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -15
- package/dist/bridge/bridge-service.d.ts +1 -1
- package/dist/bridge/bridge-service.js +7 -7
- package/dist/bridge/control.js +1 -1
- package/dist/bridge/export-store.d.ts +35 -0
- package/dist/bridge/export-store.js +195 -0
- package/dist/bridge/extension-session.d.ts +9 -2
- package/dist/bridge/extension-session.js +56 -8
- package/dist/cli.js +563 -148
- package/dist/commands.d.ts +8 -0
- package/dist/commands.js +276 -40
- package/dist/constants.d.ts +2 -2
- package/dist/constants.js +4 -2
- package/dist/http-client.d.ts +24 -0
- package/dist/http-client.js +101 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/mcp.d.ts +4 -3
- package/dist/mcp.js +221 -134
- package/dist/protocol/schema.d.ts +503 -34
- package/dist/protocol/schema.js +396 -15
- package/dist/server.d.ts +7 -4
- package/dist/server.js +70 -26
- package/dist/update-notifier-worker.d.ts +2 -0
- package/dist/update-notifier-worker.js +3 -0
- package/dist/update-notifier.d.ts +9 -0
- package/dist/update-notifier.js +178 -0
- package/dist/workflow-watch.d.ts +8 -7
- package/dist/workflow-watch.js +39 -29
- package/package.json +16 -6
- package/docs/cli.md +0 -112
- package/docs/http-api.md +0 -149
- package/docs/mcp.md +0 -90
- package/docs/operations.md +0 -85
- package/docs/workflow-watch.md +0 -86
- package/src/bridge/bridge-service.ts +0 -291
- package/src/bridge/control.ts +0 -281
- package/src/bridge/daemon.ts +0 -102
- package/src/bridge/extension-session.ts +0 -286
- package/src/bridge/pairing-state.ts +0 -92
- package/src/cli.ts +0 -1019
- package/src/commands.ts +0 -158
- package/src/constants.ts +0 -10
- package/src/errors.ts +0 -72
- package/src/index.ts +0 -35
- package/src/mcp.ts +0 -386
- package/src/protocol/schema.ts +0 -202
- package/src/server.ts +0 -623
- package/src/workflow-watch.ts +0 -364
package/dist/mcp.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { VERSION } from './constants.js';
|
|
4
|
-
import { dispatchCommand } from './commands.js';
|
|
4
|
+
import { dispatchCommand, getCommandDefinition } from './commands.js';
|
|
5
|
+
import { createHttpBridgeApi, parseBridgeUrl } from './http-client.js';
|
|
5
6
|
import { BridgeError, safeError, safeSuccess } from './errors.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
export function createBridgeMcpApi(bridge) {
|
|
7
|
+
import { WorkflowStatusEventSchema, WorkflowStatusInputSchema, WorkflowStartInputSchema, WorkflowControlInputSchema, DataInputSchema, DataGetInputSchema, DataExportInputSchema, DataListInputSchema, DataPreviewInputSchema, UserMeInputSchema, WorkflowStatusWatchResultSchema, ExtensionListResultSchema, WorkflowLogInputSchema, WorkflowLogsInputSchema, WorkflowListInputSchema, WorkflowGetInputSchema, WorkflowCreateInputSchema, WorkflowUpdateInputSchema, WorkflowGuideInputSchema, WorkflowSchemaInputSchema, WorkflowValidateInputSchema, } from './protocol/schema.js';
|
|
8
|
+
import { connectWorkflowStatusWatch, waitForWorkflowStatus, workflowStatusWatchWebSocketUrl } from './workflow-watch.js';
|
|
9
|
+
export function createBridgeMcpApi(bridge, exports) {
|
|
9
10
|
return {
|
|
10
|
-
executeCommand: (name, request) => dispatchCommand(name, request, { bridge }),
|
|
11
|
+
executeCommand: (name, request) => dispatchCommand(name, request, { bridge, exports }),
|
|
11
12
|
listExtensions: async () => bridge.getExtensionStatuses(),
|
|
12
|
-
|
|
13
|
+
watchWorkflowStatus: (input, onStatus, signal) => {
|
|
13
14
|
const clientId = bridge.resolveCommandClientId('workflow.status', input.clientId);
|
|
14
15
|
return waitForWorkflowStatus({
|
|
15
16
|
clientId,
|
|
@@ -19,11 +20,11 @@ export function createBridgeMcpApi(bridge) {
|
|
|
19
20
|
if (event.clientId === clientId && event.workflowId === input.workflowId)
|
|
20
21
|
handleStatus(event);
|
|
21
22
|
};
|
|
22
|
-
bridge.on('workflow.status', listener);
|
|
23
|
-
return () => bridge.off('workflow.status', listener);
|
|
23
|
+
bridge.on('workflow.status.changed', listener);
|
|
24
|
+
return () => bridge.off('workflow.status.changed', listener);
|
|
24
25
|
},
|
|
25
26
|
readStatus: async () => {
|
|
26
|
-
const status = await bridge.sendCommand('workflow.status
|
|
27
|
+
const status = await bridge.sendCommand('workflow.status', { workflowId: input.workflowId }, clientId);
|
|
27
28
|
return parseWorkflowStatusEvent(status, clientId, input.workflowId);
|
|
28
29
|
},
|
|
29
30
|
onStatus,
|
|
@@ -35,40 +36,11 @@ export function createBridgeMcpApi(bridge) {
|
|
|
35
36
|
}
|
|
36
37
|
export function createHttpMcpApi(baseUrl, token) {
|
|
37
38
|
const bridgeUrl = parseBridgeUrl(baseUrl);
|
|
38
|
-
const
|
|
39
|
-
if (token) {
|
|
40
|
-
headers.Authorization = `Bearer ${token}`;
|
|
41
|
-
}
|
|
42
|
-
const executeCommand = async (name, request) => {
|
|
43
|
-
const response = await fetchBridge(new URL(`/v1/commands/${encodeURIComponent(name)}`, bridgeUrl), {
|
|
44
|
-
method: 'POST',
|
|
45
|
-
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
46
|
-
body: JSON.stringify(request),
|
|
47
|
-
});
|
|
48
|
-
const body = await readBridgeResponse(response);
|
|
49
|
-
if (!response.ok || !body.ok) {
|
|
50
|
-
throw new BridgeError(body.error?.code ?? 'BridgeRequestFailed', body.error?.message ?? response.statusText, {
|
|
51
|
-
status: response.status,
|
|
52
|
-
details: body.error?.details,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
return body.data;
|
|
56
|
-
};
|
|
39
|
+
const api = createHttpBridgeApi(baseUrl, token);
|
|
57
40
|
return {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const body = await readBridgeResponse(response);
|
|
62
|
-
if (!response.ok || !Array.isArray(body.extensions)) {
|
|
63
|
-
throw new BridgeError(body.error?.code ?? 'BridgeRequestFailed', body.error?.message ?? response.statusText, {
|
|
64
|
-
status: response.status,
|
|
65
|
-
details: body.error?.details,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
return body.extensions;
|
|
69
|
-
},
|
|
70
|
-
watchWorkflow: async (input, onStatus, signal) => {
|
|
71
|
-
const connection = await connectWorkflowWatch(workflowWatchWebSocketUrl(bridgeUrl), { clientId: input.clientId, workflowId: input.workflowId }, token, signal);
|
|
41
|
+
...api,
|
|
42
|
+
watchWorkflowStatus: async (input, onStatus, signal) => {
|
|
43
|
+
const connection = await connectWorkflowStatusWatch(workflowStatusWatchWebSocketUrl(bridgeUrl), { clientId: input.clientId, workflowId: input.workflowId }, token, signal);
|
|
72
44
|
try {
|
|
73
45
|
return await waitForWorkflowStatus({
|
|
74
46
|
clientId: connection.clientId,
|
|
@@ -82,7 +54,7 @@ export function createHttpMcpApi(baseUrl, token) {
|
|
|
82
54
|
};
|
|
83
55
|
},
|
|
84
56
|
readStatus: async () => {
|
|
85
|
-
const status = await executeCommand('workflow.status', {
|
|
57
|
+
const status = await api.executeCommand('workflow.status', {
|
|
86
58
|
clientId: connection.clientId,
|
|
87
59
|
input: { workflowId: input.workflowId },
|
|
88
60
|
});
|
|
@@ -101,88 +73,240 @@ export function createHttpMcpApi(baseUrl, token) {
|
|
|
101
73
|
}
|
|
102
74
|
export function createTapickerMcpServer(api) {
|
|
103
75
|
const server = new McpServer({ name: 'tapicker-bridge', version: VERSION });
|
|
104
|
-
server.registerTool('
|
|
76
|
+
server.registerTool('extension_list', {
|
|
105
77
|
title: 'List connected Extensions',
|
|
106
|
-
description: 'List connected Tapicker Extensions and their
|
|
78
|
+
description: 'List connected Tapicker Extensions and their account snapshots when available. Choose a ready Extension by username or client name and use its clientId in subsequent commands.',
|
|
107
79
|
inputSchema: z.object({}).strict(),
|
|
80
|
+
outputSchema: z.object({ ok: z.literal(true), data: ExtensionListResultSchema }).strict(),
|
|
81
|
+
annotations: { readOnlyHint: true },
|
|
82
|
+
}, async () => runTool(async () => ({ extensions: await api.listExtensions() })));
|
|
83
|
+
server.registerTool('workflow_guide', {
|
|
84
|
+
title: 'Read Workflow authoring guide',
|
|
85
|
+
description: 'Before generating or modifying Tapicker Workflow nodes, read topic overview (the default). Read related node and shared documentation by calling this tool with references.topic from a previous result. content preserves Markdown; source links are implementation references. Reuse the returned clientId for later calls.',
|
|
86
|
+
outputSchema: commandOutputSchema('workflow.guide'),
|
|
87
|
+
inputSchema: WorkflowGuideInputSchema.safeExtend({
|
|
88
|
+
clientId: z
|
|
89
|
+
.uuid()
|
|
90
|
+
.optional()
|
|
91
|
+
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
92
|
+
}),
|
|
93
|
+
annotations: { readOnlyHint: true },
|
|
94
|
+
}, async ({ clientId, ...input }) => runTool(() => api.executeCommand('workflow.guide', { clientId, input })));
|
|
95
|
+
server.registerTool('workflow_schema', {
|
|
96
|
+
title: 'Get Workflow document schema',
|
|
97
|
+
description: 'Get the JSON Schema for building or editing Tapicker Workflow documents. Read workflow_guide for node behavior, then pass the nodeTypes you plan to use, such as NodeLoop and NodeExtractData. Omit nodeTypes for all supported business nodes. Returns a self-contained draft 2020-12 schema in schema. Defaults do not fill omitted fields. Check generated documents with workflow_validate; schema validation does not prove real-page execution. Reuse the returned clientId.',
|
|
98
|
+
outputSchema: commandOutputSchema('workflow.schema'),
|
|
99
|
+
inputSchema: WorkflowSchemaInputSchema.safeExtend({
|
|
100
|
+
clientId: z
|
|
101
|
+
.uuid()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
104
|
+
}),
|
|
105
|
+
annotations: { readOnlyHint: true },
|
|
106
|
+
}, async ({ clientId, ...input }) => runTool(() => api.executeCommand('workflow.schema', { clientId, input })));
|
|
107
|
+
server.registerTool('workflow_validate', {
|
|
108
|
+
title: 'Validate Workflow document',
|
|
109
|
+
description: 'Validate an Editor clipboard document before pasting, creating, or updating a Workflow. In fragment, pass a parsed version 1 tapicker/nodes document with nodes, or a tapicker/workflow document with a workflow object containing at least one of name, intro, href, or nodes. Use workflow_schema for the document shapes. Fix any issues identified by path, code, and optional nodeId. On success, normalized nodes are returned when supplied. Pass nodes directly to workflow_create or workflow_update. Does not save, run, access pages, execute templates, or request browser permissions. Static validation does not prove real-page execution. Reuse the returned clientId.',
|
|
110
|
+
outputSchema: commandOutputSchema('workflow.validate'),
|
|
111
|
+
inputSchema: WorkflowValidateInputSchema.safeExtend({
|
|
112
|
+
clientId: z
|
|
113
|
+
.uuid()
|
|
114
|
+
.optional()
|
|
115
|
+
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
116
|
+
}),
|
|
117
|
+
annotations: { readOnlyHint: true },
|
|
118
|
+
}, async ({ clientId, ...input }) => runTool(() => api.executeCommand('workflow.validate', { clientId, input })));
|
|
119
|
+
server.registerTool('workflow_list', {
|
|
120
|
+
title: 'List Workflows',
|
|
121
|
+
description: 'Find your Workflows through the connected Extension account. scope defaults to own; shared includes accessible shared/public Workflows, pinned lists pinned entries. name filters fullname by substring. host matches the exact site host, such as example.com or localhost:3000, without a scheme or path; it excludes generic * Workflows unless host is *. Omit host for all sites. name and host filters are combined. page defaults to 1 and pageSize to 20 (max 100). Returns summaries identified by id and fullname, without nodes; an empty workflows array ends pagination. Do not run or edit deleted entries. Use id as workflowId in later calls. Reading nodes with workflow_get requires view, edit, or manage permission. Reuse the returned clientId.',
|
|
122
|
+
outputSchema: commandOutputSchema('workflow.list'),
|
|
123
|
+
inputSchema: WorkflowListInputSchema.safeExtend({
|
|
124
|
+
clientId: z
|
|
125
|
+
.uuid()
|
|
126
|
+
.optional()
|
|
127
|
+
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
128
|
+
}),
|
|
108
129
|
annotations: { readOnlyHint: true },
|
|
109
|
-
}, async () => runTool(() => api.
|
|
130
|
+
}, async ({ clientId, ...input }) => runTool(() => api.executeCommand('workflow.list', { clientId, input })));
|
|
131
|
+
server.registerTool('workflow_get', {
|
|
132
|
+
title: 'Read Workflow',
|
|
133
|
+
description: 'Read the latest saved Workflow metadata and startup argument definitions in workflow.args. Pass the id returned by workflow_list. Use the declared names and types to supply values to workflow_start. With use-only permission, workflow.nodes is empty because the saved nodes are hidden. View, edit, or manage permission returns the full nodes. To update with edit or manage permission, pass the returned workflow.revision when available and reuse clientId.',
|
|
134
|
+
outputSchema: commandOutputSchema('workflow.get'),
|
|
135
|
+
inputSchema: WorkflowGetInputSchema.safeExtend({
|
|
136
|
+
clientId: z
|
|
137
|
+
.uuid()
|
|
138
|
+
.optional()
|
|
139
|
+
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
140
|
+
}),
|
|
141
|
+
annotations: { readOnlyHint: true },
|
|
142
|
+
}, async ({ clientId, ...input }) => runTool(() => api.executeCommand('workflow.get', { clientId, input })));
|
|
143
|
+
server.registerTool('workflow_create', {
|
|
144
|
+
title: 'Create Workflow',
|
|
145
|
+
description: 'Create a private Workflow in the selected Extension account. Use workflow_update to change an existing Workflow. Provide name and nodes. Use workflow_schema to look up node fields and workflow_validate to check a document before saving. Creation validates and saves without running. Reuse workflowId and clientId for later calls. If the response is lost, search workflow_list with scope own and the requested name in the same Extension, then use workflow_get to verify matching content before retrying.',
|
|
146
|
+
outputSchema: commandOutputSchema('workflow.create'),
|
|
147
|
+
inputSchema: WorkflowCreateInputSchema.safeExtend({
|
|
148
|
+
clientId: z
|
|
149
|
+
.uuid()
|
|
150
|
+
.optional()
|
|
151
|
+
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
152
|
+
}),
|
|
153
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
154
|
+
}, async ({ clientId, ...input }) => runTool(() => api.executeCommand('workflow.create', { clientId, input })));
|
|
155
|
+
server.registerTool('workflow_update', {
|
|
156
|
+
title: 'Update Workflow',
|
|
157
|
+
description: 'Update a saved Workflow with edit or manage permission. First call workflow_get, then reuse its clientId and pass the Workflow ID and revision you read. Provide at least one of name, intro, href, or nodes. Omitted fields are preserved. Supplying nodes replaces the entire node tree after validation. Returns the new revision without running the Workflow. On WorkflowConflict, read the latest content and reconcile your changes before retrying. If the response is lost, read the Workflow to check whether the changes were saved.',
|
|
158
|
+
outputSchema: commandOutputSchema('workflow.update'),
|
|
159
|
+
inputSchema: WorkflowUpdateInputSchema.safeExtend({
|
|
160
|
+
clientId: z
|
|
161
|
+
.uuid()
|
|
162
|
+
.optional()
|
|
163
|
+
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
164
|
+
}),
|
|
165
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
|
|
166
|
+
}, async ({ clientId, ...input }) => runTool(() => api.executeCommand('workflow.update', { clientId, input })));
|
|
110
167
|
server.registerTool('workflow_start', {
|
|
111
168
|
title: 'Start Workflow',
|
|
112
|
-
description: 'Start a Workflow in
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
workflowId: z.string().min(1).describe('The Workflow ID to start.'),
|
|
116
|
-
args: z.record(z.string(), z.unknown()).optional().describe('Optional Workflow input arguments.'),
|
|
117
|
-
tab: TabTargetSchema.optional().describe('Browser tab target; defaults to the active tab.'),
|
|
169
|
+
description: 'Start a saved Workflow in the selected Extension. Use workflow_get to discover its startup argument definitions, then pass input values in args keyed by their exact declared names. Omit values to use the Workflow defaults. Startup uses the active browser tab as its starting point. The receipt includes runAt for the accepted run. Reuse clientId and workflowId with workflow_status_watch, and compare runAt with the receipt. If the response is lost, check workflow_status before retrying. Set args.isClearData to true only to clear existing saved tables used by this Workflow and its enabled child Workflows.',
|
|
170
|
+
outputSchema: commandOutputSchema('workflow.start'),
|
|
171
|
+
inputSchema: WorkflowStartInputSchema.safeExtend({
|
|
118
172
|
clientId: z
|
|
119
173
|
.uuid()
|
|
120
174
|
.optional()
|
|
121
175
|
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
122
|
-
})
|
|
123
|
-
.strict(),
|
|
176
|
+
}),
|
|
124
177
|
annotations: { destructiveHint: true },
|
|
125
178
|
}, async ({ clientId, ...input }) => runTool(() => api.executeCommand('workflow.start', { clientId, input })));
|
|
179
|
+
const controlDescriptions = {
|
|
180
|
+
pause: 'Pause the active Workflow instance at its next pause point. An already paused instance stays paused.',
|
|
181
|
+
resume: 'Resume the same paused Workflow instance. An already running instance continues. Use workflow_start to begin a new run after it stops.',
|
|
182
|
+
stop: 'Request a stop for the active Workflow instance, including a paused instance. Succeeds when there is no active instance. Use workflow_status_watch to confirm cleanup by waiting for stopped.',
|
|
183
|
+
};
|
|
126
184
|
for (const action of ['pause', 'resume', 'stop']) {
|
|
127
185
|
server.registerTool(`workflow_${action}`, {
|
|
128
186
|
title: `${capitalize(action)} Workflow`,
|
|
129
|
-
description: `${
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
workflowId: z.string().min(1).describe('The Workflow ID to control.'),
|
|
187
|
+
description: `${controlDescriptions[action]} Reuse workflowId and clientId from the start receipt.`,
|
|
188
|
+
outputSchema: commandOutputSchema(`workflow.${action}`),
|
|
189
|
+
inputSchema: WorkflowControlInputSchema.safeExtend({
|
|
133
190
|
clientId: z
|
|
134
191
|
.uuid()
|
|
135
192
|
.optional()
|
|
136
193
|
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
137
|
-
})
|
|
138
|
-
.strict(),
|
|
194
|
+
}),
|
|
139
195
|
annotations: action === 'stop' ? { destructiveHint: true } : undefined,
|
|
140
196
|
}, async ({ clientId, ...input }) => runTool(() => api.executeCommand(`workflow.${action}`, { clientId, input })));
|
|
141
197
|
}
|
|
142
198
|
server.registerTool('workflow_status', {
|
|
143
|
-
title: 'Get Workflow
|
|
144
|
-
description: '
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
workflowId: z.string().min(1).describe('The Workflow ID to query.'),
|
|
199
|
+
title: 'Get Workflow Status',
|
|
200
|
+
description: 'Read the latest recorded status for a Workflow in the selected Extension. Reuse workflowId and clientId from the start receipt. Use runAt, when present, to identify the run or query its logs with workflow_logs. A stopped run succeeded only if reason is completed. Use workflow_status_watch to wait for execution to finish. After it stops, pass includeDataSummary: true and the observed runAt to find the tables changed by that run.',
|
|
201
|
+
outputSchema: commandOutputSchema('workflow.status'),
|
|
202
|
+
inputSchema: WorkflowStatusInputSchema.safeExtend({
|
|
148
203
|
clientId: z
|
|
149
204
|
.uuid()
|
|
150
205
|
.optional()
|
|
151
206
|
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
152
|
-
})
|
|
153
|
-
.strict(),
|
|
207
|
+
}),
|
|
154
208
|
annotations: { readOnlyHint: true },
|
|
155
209
|
}, async ({ clientId, ...input }) => runTool(() => api.executeCommand('workflow.status', { clientId, input })));
|
|
210
|
+
server.registerTool('workflow_logs', {
|
|
211
|
+
title: 'Query Workflow Logs',
|
|
212
|
+
description: 'Read a page of log previews from the latest run. Without a cursor, returns the newest matching page in ascending ID order. Use before with the first returned ID for older logs, or after with the last ID for newer logs. Reuse clientId, runAt when available, and the same level filter across pages. If detailsTruncated is true, pass the entry’s id as logId to workflow_log for full details.',
|
|
213
|
+
outputSchema: commandOutputSchema('workflow.logs'),
|
|
214
|
+
inputSchema: WorkflowLogsInputSchema.safeExtend({
|
|
215
|
+
clientId: z
|
|
216
|
+
.uuid()
|
|
217
|
+
.optional()
|
|
218
|
+
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
219
|
+
}),
|
|
220
|
+
annotations: { readOnlyHint: true },
|
|
221
|
+
}, async ({ clientId, ...input }) => runTool(() => api.executeCommand('workflow.logs', { clientId, input })));
|
|
222
|
+
server.registerTool('workflow_log', {
|
|
223
|
+
title: 'Get Workflow Log',
|
|
224
|
+
description: 'Read one log entry with full readable details. Pass the entry’s id from workflow_logs as logId, and reuse its workflowId, clientId, and runAt when available.',
|
|
225
|
+
outputSchema: commandOutputSchema('workflow.log'),
|
|
226
|
+
inputSchema: WorkflowLogInputSchema.extend({
|
|
227
|
+
clientId: z
|
|
228
|
+
.uuid()
|
|
229
|
+
.optional()
|
|
230
|
+
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
231
|
+
}),
|
|
232
|
+
annotations: { readOnlyHint: true },
|
|
233
|
+
}, async ({ clientId, ...input }) => runTool(() => api.executeCommand('workflow.log', { clientId, input })));
|
|
234
|
+
server.registerTool('data_list', {
|
|
235
|
+
title: 'List Data Tables',
|
|
236
|
+
description: 'Find saved data tables by table name or exact owning Workflow ID. Each table includes its ordered columnNames. This returns metadata without consuming export quota. Defaults to page 1 with 20 tables. Continue with the next page when hasMore is true. Reuse clientId and tableId with data_preview to inspect a sample or data_get to read rows.',
|
|
237
|
+
outputSchema: commandOutputSchema('data.list'),
|
|
238
|
+
inputSchema: DataListInputSchema.safeExtend({
|
|
239
|
+
clientId: z
|
|
240
|
+
.uuid()
|
|
241
|
+
.optional()
|
|
242
|
+
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
243
|
+
}),
|
|
244
|
+
annotations: { readOnlyHint: true },
|
|
245
|
+
}, async ({ clientId, ...input }) => runTool(() => api.executeCommand('data.list', { clientId, input })));
|
|
246
|
+
server.registerTool('data_preview', {
|
|
247
|
+
title: 'Preview Extracted Data',
|
|
248
|
+
description: 'Inspect up to 10 saved rows without consuming export quota. Use tableId from data_list or a run data summary. sample defaults to oldest; latest selects the most recently inserted rows. Both return rows in ascending ID order. columnNames and rowCount describe the table. isTruncated indicates the size limit omitted sample rows. Use data_get for more rows.',
|
|
249
|
+
outputSchema: commandOutputSchema('data.preview'),
|
|
250
|
+
inputSchema: DataPreviewInputSchema.safeExtend({
|
|
251
|
+
clientId: z
|
|
252
|
+
.uuid()
|
|
253
|
+
.optional()
|
|
254
|
+
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
255
|
+
}),
|
|
256
|
+
annotations: { readOnlyHint: true },
|
|
257
|
+
}, async ({ clientId, ...input }) => runTool(() => api.executeCommand('data.preview', { clientId, input })));
|
|
156
258
|
server.registerTool('data_get', {
|
|
157
|
-
title: 'Get
|
|
158
|
-
description: '
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
tableId: z.string().trim().min(1).max(128).describe('The data table ID copied from Tapicker Data.'),
|
|
259
|
+
title: 'Get Extracted Data',
|
|
260
|
+
description: 'Read saved rows from Tapicker Data using the table ID from data_list or Copy ID. Defaults to page 1 with 50 rows. Use hasMore to continue with the next page and reuse clientId and pageSize. Set all to true for a small table that fits in one response. rowCount is the total recorded for the table. Returned rows consume export quota; use data_preview for a free sample. If the response is lost, quota may already have been deducted.',
|
|
261
|
+
outputSchema: commandOutputSchema('data.get'),
|
|
262
|
+
inputSchema: DataGetInputSchema.safeExtend({
|
|
162
263
|
clientId: z
|
|
163
264
|
.uuid()
|
|
164
265
|
.optional()
|
|
165
266
|
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
}, async ({ clientId,
|
|
267
|
+
}),
|
|
268
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
269
|
+
}, async ({ clientId, ...input }) => runTool(() => api.executeCommand('data.get', { clientId, input })));
|
|
270
|
+
server.registerTool('data_export', {
|
|
271
|
+
title: 'Export Data Table',
|
|
272
|
+
description: 'Export a whole saved table as a file. Get its tableId from data_list. Choose xlsx, xls, csv, json, xml, or txt; the default is csv. The receipt contains the filename, rowCount, and an expiring downloadPath on the same Bridge. Download with the same Bridge authorization. Exporting consumes row quota; downloading the same file again does not. If the export response is lost, quota may already have been deducted.',
|
|
273
|
+
outputSchema: commandOutputSchema('data.export'),
|
|
274
|
+
inputSchema: DataExportInputSchema.safeExtend({
|
|
275
|
+
clientId: z
|
|
276
|
+
.uuid()
|
|
277
|
+
.optional()
|
|
278
|
+
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
279
|
+
}),
|
|
280
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
281
|
+
}, async ({ clientId, ...input }) => runTool(() => api.executeCommand('data.export', { clientId, input })));
|
|
169
282
|
server.registerTool('data_delete', {
|
|
170
|
-
title: 'Delete
|
|
171
|
-
description: 'Delete an
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
tableId: z.string().trim().min(1).max(128).describe('The data table ID copied from Tapicker Data.'),
|
|
283
|
+
title: 'Delete Extracted Data',
|
|
284
|
+
description: 'Delete an entire saved table and its rows from Tapicker Data. Pass the table ID from Copy ID and select the Extension that holds the table. Confirm the intended table before deleting. On DataTableInUse, wait for the current operation to finish. If the response is lost, verify the table before retrying, since a later Workflow run can recreate it under the same ID.',
|
|
285
|
+
outputSchema: commandOutputSchema('data.delete'),
|
|
286
|
+
inputSchema: DataInputSchema.safeExtend({
|
|
175
287
|
clientId: z
|
|
176
288
|
.uuid()
|
|
177
289
|
.optional()
|
|
178
290
|
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
annotations: { destructiveHint: true },
|
|
291
|
+
}),
|
|
292
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
|
|
182
293
|
}, async ({ clientId, tableId }) => runTool(() => api.executeCommand('data.delete', { clientId, input: { tableId } })));
|
|
183
|
-
server.registerTool('
|
|
184
|
-
title: '
|
|
185
|
-
description: '
|
|
294
|
+
server.registerTool('user_me', {
|
|
295
|
+
title: 'Get Current Tapicker User',
|
|
296
|
+
description: 'Read the current Tapicker account and plan for the selected Extension. Returns user with id, username, and plan. On SignInRequired, ask the user to sign in to Tapicker in that Extension. EmailNotVerified and AccountBanned identify account restrictions.',
|
|
297
|
+
outputSchema: commandOutputSchema('user.me'),
|
|
298
|
+
inputSchema: UserMeInputSchema.safeExtend({
|
|
299
|
+
clientId: z
|
|
300
|
+
.uuid()
|
|
301
|
+
.optional()
|
|
302
|
+
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
303
|
+
}),
|
|
304
|
+
annotations: { readOnlyHint: true },
|
|
305
|
+
}, async ({ clientId }) => runTool(() => api.executeCommand('user.me', { clientId, input: {} })));
|
|
306
|
+
server.registerTool('workflow_status_watch', {
|
|
307
|
+
title: 'Watch Workflow Status',
|
|
308
|
+
description: 'Subscribe first, then query the current status and wait for changes until stopped, timeout, or cancellation. Does not start the Workflow; an already stopped snapshot returns immediately. Cancellation only stops waiting. The result has isTimedOut set to true when the wait reaches its time limit. reason identifies how execution ended. If multiple Extensions are connected, provide clientId.',
|
|
309
|
+
outputSchema: z.object({ ok: z.literal(true), data: WorkflowStatusWatchResultSchema }).strict(),
|
|
186
310
|
inputSchema: z
|
|
187
311
|
.object({
|
|
188
312
|
workflowId: z.string().min(1).describe('The Workflow ID to watch.'),
|
|
@@ -205,7 +329,7 @@ export function createTapickerMcpServer(api) {
|
|
|
205
329
|
let progress = 0;
|
|
206
330
|
let notificationQueue = Promise.resolve();
|
|
207
331
|
return runTool(async () => {
|
|
208
|
-
const result = await api.
|
|
332
|
+
const result = await api.watchWorkflowStatus({ clientId, workflowId, timeoutMs }, (event) => {
|
|
209
333
|
if (progressToken === undefined)
|
|
210
334
|
return;
|
|
211
335
|
progress += 1;
|
|
@@ -215,7 +339,7 @@ export function createTapickerMcpServer(api) {
|
|
|
215
339
|
params: {
|
|
216
340
|
progressToken,
|
|
217
341
|
progress,
|
|
218
|
-
message: `Workflow status: ${event.
|
|
342
|
+
message: `Workflow status: ${event.status}${event.reason ? ` (${event.reason})` : ''}`,
|
|
219
343
|
},
|
|
220
344
|
}))
|
|
221
345
|
.catch(() => undefined);
|
|
@@ -227,7 +351,7 @@ export function createTapickerMcpServer(api) {
|
|
|
227
351
|
return server;
|
|
228
352
|
}
|
|
229
353
|
function parseWorkflowStatusEvent(value, clientId, workflowId) {
|
|
230
|
-
const parsed = WorkflowStatusEventSchema.safeParse({ ...value, clientId });
|
|
354
|
+
const parsed = WorkflowStatusEventSchema.strip().safeParse({ ...value, clientId });
|
|
231
355
|
if (!parsed.success || parsed.data.workflowId !== workflowId) {
|
|
232
356
|
throw new BridgeError('InvalidExtensionResponse', 'Extension returned an invalid workflow status response', {
|
|
233
357
|
status: 502,
|
|
@@ -239,55 +363,18 @@ function parseWorkflowStatusEvent(value, clientId, workflowId) {
|
|
|
239
363
|
function capitalize(value) {
|
|
240
364
|
return `${value[0].toUpperCase()}${value.slice(1)}`;
|
|
241
365
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
catch (cause) {
|
|
248
|
-
throw new BridgeError('InvalidBridgeUrl', 'Bridge URL must be a valid HTTP or HTTPS URL', { status: 400, cause });
|
|
249
|
-
}
|
|
250
|
-
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
251
|
-
throw new BridgeError('InvalidBridgeUrl', 'Bridge URL must use HTTP or HTTPS', { status: 400 });
|
|
252
|
-
}
|
|
253
|
-
if (url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
|
|
254
|
-
throw new BridgeError('InvalidBridgeUrl', 'Bridge URL must contain only the HTTP(S) origin and port', {
|
|
255
|
-
status: 400,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
return url;
|
|
259
|
-
}
|
|
260
|
-
async function fetchBridge(url, init) {
|
|
261
|
-
try {
|
|
262
|
-
return await fetch(url, { ...init, signal: AbortSignal.timeout(30_000) });
|
|
263
|
-
}
|
|
264
|
-
catch (cause) {
|
|
265
|
-
throw new BridgeError('BridgeUnavailable', `Unable to reach Tapicker Bridge at ${url.origin}`, {
|
|
266
|
-
status: 503,
|
|
267
|
-
cause,
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
async function readBridgeResponse(response) {
|
|
272
|
-
let body;
|
|
273
|
-
try {
|
|
274
|
-
body = (await response.json());
|
|
275
|
-
}
|
|
276
|
-
catch (cause) {
|
|
277
|
-
throw new BridgeError('InvalidBridgeResponse', 'Bridge returned an invalid JSON response', {
|
|
278
|
-
status: 502,
|
|
279
|
-
cause,
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
if (!body || typeof body !== 'object') {
|
|
283
|
-
throw new BridgeError('InvalidBridgeResponse', 'Bridge returned an invalid response', { status: 502 });
|
|
284
|
-
}
|
|
285
|
-
return body;
|
|
366
|
+
/**
|
|
367
|
+
* 复用命令的结果 Schema,仅声明稳定结构,不内嵌 Workflow 节点业务定义。
|
|
368
|
+
*/
|
|
369
|
+
function commandOutputSchema(name) {
|
|
370
|
+
return z.object({ ok: z.literal(true), data: getCommandDefinition(name).resultSchema }).strict();
|
|
286
371
|
}
|
|
287
372
|
async function runTool(operation) {
|
|
288
373
|
try {
|
|
374
|
+
const result = safeSuccess(await operation());
|
|
289
375
|
return {
|
|
290
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
376
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
377
|
+
structuredContent: result,
|
|
291
378
|
};
|
|
292
379
|
}
|
|
293
380
|
catch (error) {
|