@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.
Files changed (49) hide show
  1. package/README.md +15 -15
  2. package/dist/bridge/bridge-service.d.ts +1 -1
  3. package/dist/bridge/bridge-service.js +7 -7
  4. package/dist/bridge/control.js +1 -1
  5. package/dist/bridge/export-store.d.ts +35 -0
  6. package/dist/bridge/export-store.js +195 -0
  7. package/dist/bridge/extension-session.d.ts +9 -2
  8. package/dist/bridge/extension-session.js +56 -8
  9. package/dist/cli.js +563 -148
  10. package/dist/commands.d.ts +8 -0
  11. package/dist/commands.js +276 -40
  12. package/dist/constants.d.ts +2 -2
  13. package/dist/constants.js +4 -2
  14. package/dist/http-client.d.ts +24 -0
  15. package/dist/http-client.js +101 -0
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.js +2 -2
  18. package/dist/mcp.d.ts +4 -3
  19. package/dist/mcp.js +221 -134
  20. package/dist/protocol/schema.d.ts +503 -34
  21. package/dist/protocol/schema.js +396 -15
  22. package/dist/server.d.ts +7 -4
  23. package/dist/server.js +70 -26
  24. package/dist/update-notifier-worker.d.ts +2 -0
  25. package/dist/update-notifier-worker.js +3 -0
  26. package/dist/update-notifier.d.ts +9 -0
  27. package/dist/update-notifier.js +178 -0
  28. package/dist/workflow-watch.d.ts +8 -7
  29. package/dist/workflow-watch.js +39 -29
  30. package/package.json +16 -6
  31. package/docs/cli.md +0 -112
  32. package/docs/http-api.md +0 -149
  33. package/docs/mcp.md +0 -90
  34. package/docs/operations.md +0 -85
  35. package/docs/workflow-watch.md +0 -86
  36. package/src/bridge/bridge-service.ts +0 -291
  37. package/src/bridge/control.ts +0 -281
  38. package/src/bridge/daemon.ts +0 -102
  39. package/src/bridge/extension-session.ts +0 -286
  40. package/src/bridge/pairing-state.ts +0 -92
  41. package/src/cli.ts +0 -1019
  42. package/src/commands.ts +0 -158
  43. package/src/constants.ts +0 -10
  44. package/src/errors.ts +0 -72
  45. package/src/index.ts +0 -35
  46. package/src/mcp.ts +0 -386
  47. package/src/protocol/schema.ts +0 -202
  48. package/src/server.ts +0 -623
  49. 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 { TabTargetSchema, WorkflowStatusEventSchema } from './protocol/schema.js';
7
- import { connectWorkflowWatch, waitForWorkflowStatus, workflowWatchWebSocketUrl } from './workflow-watch.js';
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
- watchWorkflow: (input, onStatus, signal) => {
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.get', { workflowId: input.workflowId }, clientId);
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 headers = {};
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
- executeCommand,
59
- listExtensions: async () => {
60
- const response = await fetchBridge(new URL('/v1/extensions', bridgeUrl), { headers });
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('extensions_list', {
76
+ server.registerTool('extension_list', {
105
77
  title: 'List connected Extensions',
106
- description: 'List connected Tapicker Extensions and their clientId values for targeting Workflow commands.',
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.listExtensions()));
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 a connected Tapicker Extension. Returns the workflowId, full workflowName, and target clientId. If multiple Extensions are connected, provide clientId.',
113
- inputSchema: z
114
- .object({
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: `${capitalize(action)} a Workflow by workflowId. If multiple Extensions are connected, provide clientId.`,
130
- inputSchema: z
131
- .object({
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 status',
144
- description: 'Get the current or most recently persisted status for a Workflow. If multiple Extensions are connected, provide clientId.',
145
- inputSchema: z
146
- .object({
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 extracted data',
158
- description: 'Get all rows from an extracted data table by tableId. This consumes the export row quota of the connected Extension account. If multiple Extensions are connected, provide clientId.',
159
- inputSchema: z
160
- .object({
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
- .strict(),
168
- }, async ({ clientId, tableId }) => runTool(() => api.executeCommand('data.get', { clientId, input: { tableId } })));
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 extracted data',
171
- description: 'Delete an extracted data table and remove it from Tapicker Data by tableId. If multiple Extensions are connected, provide clientId.',
172
- inputSchema: z
173
- .object({
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
- .strict(),
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('workflow_watch', {
184
- title: 'Wait for Workflow completion',
185
- description: 'Listen for Workflow status changes until it completes, stops, or fails. Returns the latest status when timeoutMs is reached. If multiple Extensions are connected, provide clientId.',
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.watchWorkflow({ clientId, workflowId, timeoutMs }, (event) => {
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.state}`,
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
- function parseBridgeUrl(value) {
243
- let url;
244
- try {
245
- url = new URL(value);
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(safeSuccess(await operation()), null, 2) }],
376
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
377
+ structuredContent: result,
291
378
  };
292
379
  }
293
380
  catch (error) {