@tapicker/bridge 1.0.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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/dist/bridge/bridge-service.d.ts +46 -0
  4. package/dist/bridge/bridge-service.js +245 -0
  5. package/dist/bridge/control.d.ts +7 -0
  6. package/dist/bridge/control.js +211 -0
  7. package/dist/bridge/daemon.d.ts +16 -0
  8. package/dist/bridge/daemon.js +97 -0
  9. package/dist/bridge/extension-session.d.ts +43 -0
  10. package/dist/bridge/extension-session.js +204 -0
  11. package/dist/bridge/pairing-state.d.ts +22 -0
  12. package/dist/bridge/pairing-state.js +76 -0
  13. package/dist/cli.d.ts +3 -0
  14. package/dist/cli.js +835 -0
  15. package/dist/commands.d.ts +33 -0
  16. package/dist/commands.js +123 -0
  17. package/dist/constants.d.ts +10 -0
  18. package/dist/constants.js +10 -0
  19. package/dist/errors.d.ts +27 -0
  20. package/dist/errors.js +52 -0
  21. package/dist/index.d.ts +10 -0
  22. package/dist/index.js +8 -0
  23. package/dist/mcp.d.ts +13 -0
  24. package/dist/mcp.js +300 -0
  25. package/dist/protocol/schema.d.ts +149 -0
  26. package/dist/protocol/schema.js +155 -0
  27. package/dist/server.d.ts +63 -0
  28. package/dist/server.js +486 -0
  29. package/dist/workflow-watch.d.ts +31 -0
  30. package/dist/workflow-watch.js +304 -0
  31. package/docs/cli.md +112 -0
  32. package/docs/http-api.md +149 -0
  33. package/docs/mcp.md +90 -0
  34. package/docs/operations.md +85 -0
  35. package/docs/workflow-watch.md +86 -0
  36. package/package.json +55 -0
  37. package/src/bridge/bridge-service.ts +291 -0
  38. package/src/bridge/control.ts +281 -0
  39. package/src/bridge/daemon.ts +102 -0
  40. package/src/bridge/extension-session.ts +286 -0
  41. package/src/bridge/pairing-state.ts +92 -0
  42. package/src/cli.ts +1019 -0
  43. package/src/commands.ts +158 -0
  44. package/src/constants.ts +10 -0
  45. package/src/errors.ts +72 -0
  46. package/src/index.ts +35 -0
  47. package/src/mcp.ts +386 -0
  48. package/src/protocol/schema.ts +202 -0
  49. package/src/server.ts +623 -0
  50. package/src/workflow-watch.ts +364 -0
@@ -0,0 +1,304 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { WebSocket } from 'ws';
3
+ import { DEFAULT_WORKFLOW_WATCH_WS_PATH } from './constants.js';
4
+ import { BridgeError } from './errors.js';
5
+ import { SafeResultSchema, WorkflowStatusEventSchema, createEnvelope, parseEnvelope } from './protocol/schema.js';
6
+ const terminalStates = new Set(['completed', 'stopped', 'failed']);
7
+ export function workflowWatchWebSocketUrl(bridgeUrl) {
8
+ const url = new URL(DEFAULT_WORKFLOW_WATCH_WS_PATH, bridgeUrl);
9
+ url.protocol = bridgeUrl.protocol === 'https:' ? 'wss:' : 'ws:';
10
+ return url;
11
+ }
12
+ export function connectWorkflowWatch(url, input, token, signal) {
13
+ return new Promise((resolve, reject) => {
14
+ if (signal?.aborted) {
15
+ reject(new BridgeError('WorkflowWatchCancelled', 'Workflow watch was cancelled', { status: 499 }));
16
+ return;
17
+ }
18
+ const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
19
+ const socket = new WebSocket(url, headers ? { headers } : undefined);
20
+ const requestId = randomUUID();
21
+ const statusListeners = new Set();
22
+ const closeListeners = new Set();
23
+ const pendingStatusEvents = [];
24
+ let acceptedClientId;
25
+ let settled = false;
26
+ let intentionalClose = false;
27
+ let closedError;
28
+ let abortListener;
29
+ const cleanupAbortListener = () => {
30
+ if (abortListener)
31
+ signal?.removeEventListener('abort', abortListener);
32
+ abortListener = undefined;
33
+ };
34
+ const failBeforeAccept = (error) => {
35
+ if (settled)
36
+ return;
37
+ settled = true;
38
+ cleanupAbortListener();
39
+ socket.terminate();
40
+ reject(error);
41
+ };
42
+ const notifyClose = (error) => {
43
+ closedError = error;
44
+ cleanupAbortListener();
45
+ for (const listener of closeListeners) {
46
+ try {
47
+ listener(error);
48
+ }
49
+ catch {
50
+ // A consumer callback must not escape the WebSocket event handler.
51
+ }
52
+ }
53
+ closeListeners.clear();
54
+ };
55
+ const close = (reason) => {
56
+ intentionalClose = true;
57
+ cleanupAbortListener();
58
+ if (socket.readyState === WebSocket.OPEN) {
59
+ socket.close(1000, 'Workflow watch closed');
60
+ }
61
+ else if (socket.readyState === WebSocket.CONNECTING) {
62
+ socket.terminate();
63
+ }
64
+ if (acceptedClientId) {
65
+ notifyClose(reason ?? new BridgeError('WorkflowWatchClosed', 'Workflow watch was closed by the client', { status: 499 }));
66
+ }
67
+ };
68
+ abortListener = () => {
69
+ const error = new BridgeError('WorkflowWatchCancelled', 'Workflow watch was cancelled', { status: 499 });
70
+ if (!acceptedClientId) {
71
+ failBeforeAccept(error);
72
+ }
73
+ else {
74
+ close(error);
75
+ }
76
+ };
77
+ signal?.addEventListener('abort', abortListener, { once: true });
78
+ socket.once('open', () => {
79
+ socket.send(JSON.stringify(createEnvelope('workflow.watch', {
80
+ clientId: input.clientId,
81
+ input: { workflowId: input.workflowId },
82
+ }, { id: requestId })));
83
+ });
84
+ socket.on('message', (raw) => {
85
+ const parsedEnvelope = parseEnvelope(raw);
86
+ if (!parsedEnvelope.success) {
87
+ const error = new BridgeError('InvalidBridgeResponse', 'Bridge returned an invalid Workflow watch message', {
88
+ status: 502,
89
+ cause: parsedEnvelope.error,
90
+ });
91
+ if (!acceptedClientId)
92
+ failBeforeAccept(error);
93
+ else {
94
+ socket.close(1007, 'Invalid Workflow watch message');
95
+ notifyClose(error);
96
+ }
97
+ return;
98
+ }
99
+ const message = parsedEnvelope.data;
100
+ if (message.type === 'workflow.watch:res' && message.id === requestId && !acceptedClientId) {
101
+ const result = SafeResultSchema.safeParse(message.data);
102
+ if (!result.success) {
103
+ failBeforeAccept(new BridgeError('InvalidBridgeResponse', 'Bridge returned an invalid Workflow watch response', {
104
+ status: 502,
105
+ }));
106
+ return;
107
+ }
108
+ if (!result.data.ok) {
109
+ failBeforeAccept(new BridgeError(result.data.error.code, result.data.error.message, {
110
+ status: 409,
111
+ details: result.data.error.details,
112
+ }));
113
+ return;
114
+ }
115
+ const accepted = WorkflowStatusEventSchema.pick({ clientId: true, workflowId: true }).safeParse(result.data.data);
116
+ if (!accepted.success || accepted.data.workflowId !== input.workflowId) {
117
+ failBeforeAccept(new BridgeError('InvalidBridgeResponse', 'Bridge accepted a different Workflow watch target', {
118
+ status: 502,
119
+ }));
120
+ return;
121
+ }
122
+ acceptedClientId = accepted.data.clientId;
123
+ settled = true;
124
+ const connection = {
125
+ clientId: accepted.data.clientId,
126
+ workflowId: accepted.data.workflowId,
127
+ onStatus(listener) {
128
+ statusListeners.add(listener);
129
+ for (const event of pendingStatusEvents.splice(0))
130
+ listener(event);
131
+ return () => statusListeners.delete(listener);
132
+ },
133
+ onClose(listener) {
134
+ if (closedError) {
135
+ listener(closedError);
136
+ return () => undefined;
137
+ }
138
+ closeListeners.add(listener);
139
+ return () => closeListeners.delete(listener);
140
+ },
141
+ close,
142
+ };
143
+ resolve(connection);
144
+ return;
145
+ }
146
+ if (message.type !== 'workflow.status' || !acceptedClientId)
147
+ return;
148
+ const event = WorkflowStatusEventSchema.safeParse(message.data);
149
+ if (!event.success)
150
+ return;
151
+ if (event.data.clientId !== acceptedClientId || event.data.workflowId !== input.workflowId)
152
+ return;
153
+ if (statusListeners.size === 0) {
154
+ if (pendingStatusEvents.length >= 128)
155
+ pendingStatusEvents.shift();
156
+ pendingStatusEvents.push(event.data);
157
+ }
158
+ else {
159
+ for (const listener of statusListeners) {
160
+ try {
161
+ listener(event.data);
162
+ }
163
+ catch {
164
+ // A consumer callback must not escape the WebSocket event handler.
165
+ }
166
+ }
167
+ }
168
+ });
169
+ socket.once('unexpected-response', (_request, response) => {
170
+ const chunks = [];
171
+ response.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
172
+ response.once('end', () => {
173
+ const body = Buffer.concat(chunks).toString('utf8');
174
+ let errorCode = 'BridgeRequestFailed';
175
+ let errorMessage = response.statusMessage || 'Bridge rejected the Workflow watch connection';
176
+ let details;
177
+ try {
178
+ const parsed = JSON.parse(body);
179
+ errorCode = parsed.error?.code || errorCode;
180
+ errorMessage = parsed.error?.message || errorMessage;
181
+ details = parsed.error?.details;
182
+ }
183
+ catch {
184
+ // Keep the HTTP status text when the response is not Bridge JSON.
185
+ }
186
+ failBeforeAccept(new BridgeError(errorCode, errorMessage, { status: response.statusCode || 503, details }));
187
+ });
188
+ });
189
+ socket.on('error', (cause) => {
190
+ const error = new BridgeError('BridgeUnavailable', 'Unable to connect to Tapicker Bridge Workflow watch', {
191
+ status: 503,
192
+ cause,
193
+ });
194
+ if (!acceptedClientId)
195
+ failBeforeAccept(error);
196
+ else
197
+ notifyClose(error);
198
+ });
199
+ socket.once('close', (code, reason) => {
200
+ cleanupAbortListener();
201
+ if (!acceptedClientId) {
202
+ failBeforeAccept(new BridgeError('WorkflowWatchClosed', 'Bridge closed the Workflow watch connection before accepting it', {
203
+ status: 503,
204
+ details: { code, reason: reason.toString() },
205
+ }));
206
+ }
207
+ else if (!intentionalClose) {
208
+ notifyClose(new BridgeError('WorkflowWatchClosed', 'Bridge closed the Workflow watch connection', {
209
+ status: 503,
210
+ details: { code, reason: reason.toString() },
211
+ }));
212
+ }
213
+ });
214
+ });
215
+ }
216
+ export function waitForWorkflowStatus(options) {
217
+ return new Promise((resolve, reject) => {
218
+ let settled = false;
219
+ let latestStatus;
220
+ let timer;
221
+ let unsubscribe = () => { };
222
+ let abortListener;
223
+ const cleanup = () => {
224
+ unsubscribe();
225
+ if (timer)
226
+ clearTimeout(timer);
227
+ if (abortListener)
228
+ options.signal?.removeEventListener('abort', abortListener);
229
+ };
230
+ const finish = (status, timedOut = false) => {
231
+ if (settled)
232
+ return;
233
+ settled = true;
234
+ cleanup();
235
+ resolve({ ...status, timedOut });
236
+ };
237
+ const fail = (error) => {
238
+ if (settled)
239
+ return;
240
+ settled = true;
241
+ cleanup();
242
+ reject(error);
243
+ };
244
+ const publish = (status) => {
245
+ if (settled)
246
+ return;
247
+ if (status.clientId !== options.clientId || status.workflowId !== options.workflowId)
248
+ return;
249
+ if (latestStatus && sameStatus(latestStatus, status))
250
+ return;
251
+ latestStatus = status;
252
+ try {
253
+ options.onStatus?.(status);
254
+ }
255
+ catch (error) {
256
+ fail(error);
257
+ return;
258
+ }
259
+ if (terminalStates.has(status.state))
260
+ finish(status);
261
+ };
262
+ if (options.signal?.aborted) {
263
+ fail(new BridgeError('WorkflowWatchCancelled', 'Workflow watch was cancelled', { status: 499 }));
264
+ return;
265
+ }
266
+ unsubscribe = options.subscribe(publish, fail);
267
+ if (settled) {
268
+ unsubscribe();
269
+ return;
270
+ }
271
+ if (options.signal) {
272
+ abortListener = () => fail(new BridgeError('WorkflowWatchCancelled', 'Workflow watch was cancelled', { status: 499 }));
273
+ options.signal.addEventListener('abort', abortListener, { once: true });
274
+ }
275
+ if (options.timeoutMs !== undefined) {
276
+ timer = setTimeout(() => {
277
+ if (latestStatus)
278
+ finish(latestStatus, true);
279
+ else
280
+ fail(new BridgeError('WorkflowWatchTimeout', 'Timed out before receiving Workflow status', {
281
+ status: 504,
282
+ details: { timeoutMs: options.timeoutMs },
283
+ }));
284
+ }, options.timeoutMs);
285
+ }
286
+ void options
287
+ .readStatus()
288
+ .then((snapshot) => {
289
+ if (settled)
290
+ return;
291
+ const snapshotIsAtLeastAsNew = latestStatus !== undefined && Date.parse(snapshot.updatedAt) >= Date.parse(latestStatus.updatedAt);
292
+ if (!latestStatus ||
293
+ snapshotIsAtLeastAsNew ||
294
+ (terminalStates.has(snapshot.state) && !terminalStates.has(latestStatus.state))) {
295
+ publish(snapshot);
296
+ }
297
+ })
298
+ .catch(fail);
299
+ });
300
+ }
301
+ function sameStatus(left, right) {
302
+ return left.state === right.state && left.updatedAt === right.updatedAt;
303
+ }
304
+ //# sourceMappingURL=workflow-watch.js.map
package/docs/cli.md ADDED
@@ -0,0 +1,112 @@
1
+ # CLI Reference
2
+
3
+ The examples below assume the package is installed globally and the `tapicker` command is available in `PATH`. Start with the [Getting Started guide](https://www.tapicker.com/docs/bridge-getting-started). Run `tapicker --help` or `tapicker <command> --help` for concise command help.
4
+
5
+ ## Start and Manage Bridge
6
+
7
+ ```bash
8
+ tapicker bridge start [options]
9
+ tapicker bridge stop [--data-dir <path>]
10
+ tapicker bridge code [--refresh] [--data-dir <path>]
11
+ tapicker bridge extensions [--data-dir <path>]
12
+ ```
13
+
14
+ | Command | Purpose |
15
+ | ----------------------- | ----------------------------------------------------------------------------------------------- |
16
+ | `bridge start` | Run in the foreground; press `Ctrl+C` to stop. Prints the listening addresses and pairing code. |
17
+ | `bridge start --daemon` | Run in the background. |
18
+ | `bridge stop` | Stop a background Bridge. |
19
+ | `bridge code` | Display the current four-digit pairing code. |
20
+ | `bridge code --refresh` | Rotate the pairing code and unlock pairing after rate limiting. |
21
+ | `bridge extensions` | List connected Extension clients, including clients that have not finished pairing. |
22
+
23
+ ### `bridge start` Options
24
+
25
+ | Option | Default | Description |
26
+ | ---------------------------------------- | --------------------------------------- | ----------------------------------------------------------------------- |
27
+ | `--host <host>` | `127.0.0.1` | Bind address. Non-loopback binding requires a Token and TLS. |
28
+ | `--port <port>` | First available port from `9520`–`9530` | Set a fixed listening port. |
29
+ | `--token <token>` | `TAPICKER_BRIDGE_TOKEN` or none | Bearer Token for protected HTTP, MCP, and status subscription requests. |
30
+ | `--cors-origin <origins>` | `*` | Comma-separated allowlist of HTTP origins. |
31
+ | `--tls-key <path>` / `--tls-cert <path>` | None | TLS private key and certificate; provide both for remote binding. |
32
+ | `--data-dir <path>` | `TAPICKER_DATA_DIR` or `~/.tapicker` | Directory for pairing credentials and daemon files. |
33
+ | `--daemon` | Off | Run in the background. |
34
+
35
+ If you use a custom `--data-dir`, pass the same directory to `stop`, `code`, and `extensions`.
36
+
37
+ ## Control a Workflow
38
+
39
+ ```bash
40
+ tapicker workflow start <workflowId> [options]
41
+ tapicker workflow pause <workflowId> [options]
42
+ tapicker workflow resume <workflowId> [options]
43
+ tapicker workflow stop <workflowId> [options]
44
+ tapicker workflow status <workflowId> [options]
45
+ tapicker workflow watch <workflowId> [options]
46
+ ```
47
+
48
+ Start with an input object and stream status updates:
49
+
50
+ ```bash
51
+ tapicker workflow start jr657xeb --args '{"keyword":"Tapicker"}' --watch
52
+ ```
53
+
54
+ Other tab targets are `new` and `id:<positive-tabId>`:
55
+
56
+ ```bash
57
+ tapicker workflow start jr657xeb --tab new
58
+ tapicker workflow start jr657xeb --tab id:123
59
+ ```
60
+
61
+ `--args` must be a JSON object. The default is `{}`. `--tab` defaults to `active`.
62
+
63
+ | Option | Default | Description |
64
+ | -------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------- |
65
+ | `--host <host>` | `127.0.0.1` | Bridge host. |
66
+ | `--port <port>` | `9520` | Bridge port. Pass the actual port if Bridge selected another one. |
67
+ | `--token <token>` | None | Bridge Bearer Token. |
68
+ | `--client-id <uuid>` | Auto-select when exactly one Extension is ready | Select an Extension when multiple are ready. |
69
+ | `--https` | Off | Use HTTPS and WSS instead of HTTP and WS. |
70
+ | `--args <json>` | `{}` | Workflow input object; `start` only. |
71
+ | `--tab <target>` | `active` | `active`, `new`, or `id:<positive-tabId>`; `start` only. |
72
+ | `--watch` | Off | After starting, stream updates until a terminal state; `start` only. |
73
+ | `--timeout <milliseconds>` | No limit | Limit how long `watch` or `start --watch` waits. Does not stop the Workflow. |
74
+
75
+ `start --watch` establishes the watch before starting the Workflow to avoid missing initial status changes. `workflow watch` listens to a Workflow that is already running; it does not start or control it. A watch timeout or `Ctrl+C` ends only the listener, not the Workflow. Output is newline-delimited JSON.
76
+
77
+ The start response includes `workflowId`, the full `workflowName`, and the selected `clientId`. A Workflow supports one active instance at a time in its Extension.
78
+
79
+ ## Select an Extension
80
+
81
+ List clients first:
82
+
83
+ ```bash
84
+ tapicker bridge extensions
85
+ ```
86
+
87
+ Pass `--client-id <uuid>` to target one ready Extension. You can omit it only when exactly one ready Extension is connected. Pending pairing connections are not command targets. Bridge never broadcasts a command to every Extension.
88
+
89
+ ## Read or Delete Extracted Data
90
+
91
+ ```bash
92
+ tapicker data get <tableId> [options]
93
+ tapicker data delete <tableId> [options]
94
+ ```
95
+
96
+ Get all rows from a data table:
97
+
98
+ ```bash
99
+ tapicker data get 4a1b2c3d
100
+ ```
101
+
102
+ `tableId` is copied from the Extension’s Data list or detail page. `data get` returns `tableId`, `tableName`, the row count, and all rows. The row count consumes the connected Extension account’s export quota; if quota is insufficient or unavailable, the command fails without returning the data. `data delete` removes the table from Tapicker Data.
103
+
104
+ Data commands accept `--host`, `--port`, `--token`, `--client-id`, and `--https`.
105
+
106
+ ## Connect an MCP Client over stdio
107
+
108
+ ```bash
109
+ tapicker mcp [--url <http(s)://host:port>] [--token <token>]
110
+ ```
111
+
112
+ This starts an MCP stdio adapter that connects to an already-running Bridge over HTTP. It does not start another Bridge server. The default URL is `http://127.0.0.1:9520`; set `TAPICKER_BRIDGE_URL` and `TAPICKER_BRIDGE_TOKEN` in the MCP client environment when needed. See the [MCP guide](mcp.md) for client configuration examples.
@@ -0,0 +1,149 @@
1
+ # HTTP API Reference
2
+
3
+ Bridge serves HTTP, WebSocket, and MCP from one port. The default local base URL is `http://127.0.0.1:9520`. At startup, Bridge tries ports `9520`–`9530` and prints the actual address. Use that address in every client.
4
+
5
+ For installation and setup, see the [Getting Started guide](https://www.tapicker.com/docs/bridge-getting-started). For a short integration walkthrough, see [Connect an AI Agent](https://www.tapicker.com/docs/bridge-ai-agents).
6
+
7
+ ## Endpoints
8
+
9
+ | Method / transport | Path | Token required when configured? | Purpose |
10
+ | ------------------- | ----------------------------- | ------------------------------- | ------------------------------------------------------ |
11
+ | `GET` | `/v1/health` | No | Check Bridge health. |
12
+ | `GET` | `/v1/discovery` | No | Read Bridge metadata and the Extension WebSocket path. |
13
+ | `GET` | `/v1/capabilities` | No | Discover commands and their input schemas. |
14
+ | `GET` | `/v1/extensions` | Yes | List connected Extension clients. |
15
+ | `POST` | `/v1/commands/{command}` | Yes | Call a Workflow or data command. |
16
+ | WebSocket | `/v1/commands/workflow.watch` | Yes | Subscribe to one Workflow’s status events. |
17
+ | Streamable HTTP MCP | `/mcp` | Yes | Connect a remote MCP client. |
18
+ | WebSocket | `/ws` | Pairing credential | Extension pairing and internal command transport. |
19
+
20
+ When Bridge is started with a Token, protected requests must include:
21
+
22
+ ```http
23
+ Authorization: Bearer YOUR_TOKEN
24
+ ```
25
+
26
+ `/ws` is for Tapicker Extension connections, not external Agent commands. It uses the pairing code and saved Extension credential, not the HTTP Bearer Token.
27
+
28
+ ## CORS and Local Security
29
+
30
+ HTTP and `/mcp` allow all web origins by default (`--cors-origin *`) and support browser `OPTIONS` preflight requests. Use one comma-separated allowlist to restrict origins:
31
+
32
+ ```bash
33
+ tapicker bridge start --cors-origin https://agent.example.com,https://app.example.com
34
+ ```
35
+
36
+ CORS is not authentication. The default local Bridge has no Bearer Token, so a web page may be able to call its local HTTP API. Keep Bridge bound to `127.0.0.1`, and set an origin allowlist when browser access should be limited. WebSocket connections do not use CORS.
37
+
38
+ ## Discover Bridge and Extensions
39
+
40
+ ```bash
41
+ curl http://127.0.0.1:9520/v1/health
42
+ curl http://127.0.0.1:9520/v1/discovery
43
+ curl http://127.0.0.1:9520/v1/capabilities
44
+ curl http://127.0.0.1:9520/v1/extensions
45
+ ```
46
+
47
+ `/v1/health` reports Bridge health, not Extension readiness. `/v1/extensions` returns connected clients, for example:
48
+
49
+ ```json
50
+ {
51
+ "extensions": [
52
+ {
53
+ "clientId": "56a450e6-41f1-448e-9ea7-a2d958cbda37",
54
+ "clientName": "MacOS.Chrome.152.a1b2",
55
+ "connected": true,
56
+ "ready": true,
57
+ "pairingRequired": false,
58
+ "capabilities": ["workflow.start", "workflow.pause"]
59
+ }
60
+ ]
61
+ }
62
+ ```
63
+
64
+ Only `ready: true` clients can receive commands. The list does not expose pairing codes or saved credentials. Add the Bearer header to `/v1/extensions` when a Token is configured.
65
+
66
+ `/v1/capabilities` lists supported commands, descriptions, JSON input schemas, and the WebSocket path for `workflow.watch`.
67
+
68
+ ## Call a Command
69
+
70
+ All HTTP command requests use this shape:
71
+
72
+ ```json
73
+ {
74
+ "clientId": "optional-extension-uuid",
75
+ "input": {
76
+ "workflowId": "jr657xeb"
77
+ }
78
+ }
79
+ ```
80
+
81
+ `clientId` is optional only when exactly one ready Extension is connected. If multiple are ready, specify one; Bridge returns `409 ClientIdRequired` rather than broadcasting the command. Pending pairing connections are not eligible targets.
82
+
83
+ | Command | `input` |
84
+ | ----------------- | ---------------------------------------------------------------------------------------------------- |
85
+ | `workflow.start` | `workflowId`; optional `args` object (default `{}`); optional `tab` (default `{ "mode": "active" }`) |
86
+ | `workflow.pause` | `workflowId` |
87
+ | `workflow.resume` | `workflowId` |
88
+ | `workflow.stop` | `workflowId` |
89
+ | `workflow.status` | `workflowId` |
90
+ | `data.get` | `tableId` |
91
+ | `data.delete` | `tableId` |
92
+
93
+ `tab` can be `{ "mode": "active" }`, `{ "mode": "new" }`, or `{ "mode": "id", "tabId": 123 }`.
94
+
95
+ Start a Workflow:
96
+
97
+ ```bash
98
+ curl -X POST http://127.0.0.1:9520/v1/commands/workflow.start \
99
+ -H 'Content-Type: application/json' \
100
+ -d '{"input":{"workflowId":"jr657xeb","args":{"keyword":"Tapicker"},"tab":{"mode":"active"}}}'
101
+ ```
102
+
103
+ If a Token is configured, add `-H 'Authorization: Bearer YOUR_TOKEN'`.
104
+
105
+ Pause, resume, stop, and status use the same request format. For example:
106
+
107
+ ```bash
108
+ curl -X POST http://127.0.0.1:9520/v1/commands/workflow.pause \
109
+ -H 'Content-Type: application/json' \
110
+ -d '{"clientId":"56a450e6-41f1-448e-9ea7-a2d958cbda37","input":{"workflowId":"jr657xeb"}}'
111
+ ```
112
+
113
+ A successful start response includes the selected `clientId`, `workflowId`, full `workflowName`, and an accepted state. Other successful responses use `{ "ok": true, "data": ... }`. Errors use `{ "ok": false, "error": { "code", "message", "details?" } }`.
114
+
115
+ ## Read or Delete a Data Table
116
+
117
+ Use the `tableId` copied from the Extension’s Data list or detail page:
118
+
119
+ ```bash
120
+ curl -X POST http://127.0.0.1:9520/v1/commands/data.get \
121
+ -H 'Content-Type: application/json' \
122
+ -d '{"input":{"tableId":"4a1b2c3d"}}'
123
+ ```
124
+
125
+ `data.get` returns `tableId`, `tableName`, `count`, and all `rows`. The row count consumes the connected Extension account’s export quota. If the quota is insufficient or unavailable, Bridge returns an error without the data.
126
+
127
+ Delete a table with `POST /v1/commands/data.delete` and the same `input` shape. It removes the table from Tapicker Data; verify the `tableId` before deleting. The success response contains `tableId` and `tableName`.
128
+
129
+ ## Get Workflow Status
130
+
131
+ `workflow.status` returns one snapshot with `workflowId`, `state`, and ISO-formatted `updatedAt`. States include `running`, `paused`, `stopping`, `completed`, `stopped`, and `failed`.
132
+
133
+ For ongoing updates, subscribe to the WebSocket endpoint `/v1/commands/workflow.watch` instead of repeatedly calling `workflow.status`. After reconnecting, call `workflow.status` once to get the latest snapshot. See [Workflow Status WebSocket](workflow-watch.md).
134
+
135
+ The `workflow.watch` path requires a WebSocket upgrade; a regular HTTP request returns `405 WebSocketRequired`.
136
+
137
+ ## Common Errors
138
+
139
+ | HTTP status | Example codes | Meaning |
140
+ | ----------- | ----------------------------------------------------------------------------- | ------------------------------------------------------ |
141
+ | `400` | `InvalidArguments`, `InvalidJson` | The request body or command input is invalid. |
142
+ | `401` | `Unauthorized` | A required Bearer Token is missing or invalid. |
143
+ | `404` | `NotFound`, `CommandNotFound` | The endpoint or command does not exist. |
144
+ | `409` | `ClientIdRequired`, `ExtensionNotConnected`, `ExtensionCapabilityUnavailable` | Select a ready Extension or check its availability. |
145
+ | `502` | `ExportQuotaExceeded` | Reading the table exceeded the account’s export quota. |
146
+ | `503` | `ExtensionUnavailable` | No paired, ready Extension is connected. |
147
+ | `504` | `ExtensionRequestTimeout` | The Extension did not respond in time. |
148
+
149
+ Use the returned `error.code` and `error.message` as the source of truth for a specific failure.
package/docs/mcp.md ADDED
@@ -0,0 +1,90 @@
1
+ # MCP Guide
2
+
3
+ Tapicker Bridge exposes Workflow and data commands as MCP tools. The Bridge must already be running, and the target Tapicker Extension must be paired and ready.
4
+
5
+ For a first-time setup, follow the [Getting Started guide](https://www.tapicker.com/docs/bridge-getting-started). For an overview of the user flow, see [Connect an AI Agent](https://www.tapicker.com/docs/bridge-ai-agents).
6
+
7
+ ## Choose a Transport
8
+
9
+ | Transport | How the agent connects | Best for |
10
+ | --------------- | -------------------------------------------------------------------- | ------------------------------------------------------ |
11
+ | stdio | The agent starts `tapicker mcp`, which connects to Bridge over HTTP. | An agent running on the same computer as Bridge. |
12
+ | Streamable HTTP | The agent connects directly to `https://<host>:<port>/mcp`. | A remote Bridge and an agent that supports remote MCP. |
13
+
14
+ Both transports expose the same tools. The stdio adapter does not start a Bridge server.
15
+
16
+ ## Local MCP over stdio
17
+
18
+ Start Bridge separately, then add an MCP server entry to your agent configuration:
19
+
20
+ ```json
21
+ {
22
+ "mcpServers": {
23
+ "tapicker": {
24
+ "command": "tapicker",
25
+ "args": ["mcp"],
26
+ "env": {
27
+ "TAPICKER_BRIDGE_URL": "http://127.0.0.1:9520"
28
+ }
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ Restart or reload the agent after changing its configuration. The CLI defaults to `http://127.0.0.1:9520`; if Bridge selected another port, update `TAPICKER_BRIDGE_URL` to the actual HTTP address printed at startup.
35
+
36
+ When the Bridge uses a Token, add `TAPICKER_BRIDGE_TOKEN` to the MCP server environment. Prefer environment variables or your agent’s secret store over putting a Token in a shared config file.
37
+
38
+ If the agent cannot find `tapicker`, set `command` to the absolute path of the installed executable. When running from a source checkout, use `node` as the command and the absolute path to `dist/cli.js` followed by `mcp` as the arguments.
39
+
40
+ You can also launch the adapter manually:
41
+
42
+ ```bash
43
+ tapicker mcp --url http://127.0.0.1:9520
44
+ ```
45
+
46
+ ## Remote MCP over Streamable HTTP
47
+
48
+ The MCP endpoint uses the same port as HTTP and WebSocket:
49
+
50
+ ```text
51
+ https://bridge.example.com:9520/mcp
52
+ ```
53
+
54
+ Configure the agent’s remote MCP connection with this URL. If Bridge uses a Token, configure this request header:
55
+
56
+ ```http
57
+ Authorization: Bearer YOUR_TOKEN
58
+ ```
59
+
60
+ Remote deployments require TLS and a Bearer Token. See [Pairing, Configuration, and Security](operations.md).
61
+
62
+ ## Available Tools
63
+
64
+ | Tool | Parameters | Purpose |
65
+ | ----------------- | ------------------------------------------------ | ------------------------------------------------------------- |
66
+ | `extensions_list` | None | List Extension clients and find a target `clientId`. |
67
+ | `workflow_start` | `workflowId`, optional `args`, `tab`, `clientId` | Start a Workflow. Returns when the start request is accepted. |
68
+ | `workflow_pause` | `workflowId`, optional `clientId` | Pause an active Workflow. |
69
+ | `workflow_resume` | `workflowId`, optional `clientId` | Resume a paused Workflow. |
70
+ | `workflow_stop` | `workflowId`, optional `clientId` | Stop an active Workflow. |
71
+ | `workflow_status` | `workflowId`, optional `clientId` | Get one current or most recently persisted status snapshot. |
72
+ | `workflow_watch` | `workflowId`, optional `clientId`, `timeoutMs` | Wait for status updates until a terminal state or timeout. |
73
+ | `data_get` | `tableId`, optional `clientId` | Read all rows from a data table; consumes export quota. |
74
+ | `data_delete` | `tableId`, optional `clientId` | Delete a data table from Tapicker Data. |
75
+
76
+ `workflow_start.tab` accepts `{ "mode": "active" }`, `{ "mode": "new" }`, or `{ "mode": "id", "tabId": 123 }`; it defaults to the active tab. `args` must be an object.
77
+
78
+ `data_get` returns `tableId`, `tableName`, and all rows. The row count consumes the connected Extension account’s export quota; when the quota is insufficient or unavailable, the operation fails without returning data. `data_delete` removes the table, so verify the `tableId` first.
79
+
80
+ ## Select an Extension
81
+
82
+ If exactly one ready Extension is connected, `clientId` can be omitted. If multiple are ready, call `extensions_list` and pass the chosen `clientId` to the command. Pending pairing connections may appear in the list but are not command targets. Bridge sends each command to one Extension; it does not broadcast.
83
+
84
+ ## Watch a Long-Running Workflow
85
+
86
+ `workflow_start` returns after the Extension accepts the start request. Then call `workflow_watch` to wait for updates without repeatedly calling `workflow_status`. Its terminal states are `completed`, `stopped`, and `failed`.
87
+
88
+ When supported by the MCP client, Bridge also reports intermediate status updates as progress. A `timeoutMs` expiration returns the latest known status with `timedOut: true`. Cancelling the watch stops waiting only; it does not stop the Workflow. Use `workflow_status` when you need a single snapshot.
89
+
90
+ The equivalent HTTP/WebSocket protocol is documented in [Workflow Status WebSocket](workflow-watch.md).