@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/src/commands.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { DEFAULT_WORKFLOW_WATCH_WS_PATH } from './constants.js'
|
|
3
|
-
import { BridgeError } from './errors.js'
|
|
4
|
-
import {
|
|
5
|
-
WorkflowControlInputSchema,
|
|
6
|
-
DataInputSchema,
|
|
7
|
-
WorkflowStartInputSchema,
|
|
8
|
-
WorkflowStartResultSchema,
|
|
9
|
-
WorkflowStatusDataSchema,
|
|
10
|
-
WorkflowWatchRequestSchema,
|
|
11
|
-
} from './protocol/schema.js'
|
|
12
|
-
import type { BridgeService } from './bridge/bridge-service.js'
|
|
13
|
-
import type { WorkflowControlInput, WorkflowStartInput } from './protocol/schema.js'
|
|
14
|
-
|
|
15
|
-
export type CommandContext = {
|
|
16
|
-
bridge: BridgeService
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
type CommandDefinition = {
|
|
20
|
-
description: string
|
|
21
|
-
schema: z.ZodType
|
|
22
|
-
execute: (input: unknown, context: CommandContext, clientId?: string) => Promise<unknown>
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const CommandRequestSchema = z
|
|
26
|
-
.object({
|
|
27
|
-
clientId: z.uuid().optional(),
|
|
28
|
-
input: z.unknown(),
|
|
29
|
-
})
|
|
30
|
-
.strict()
|
|
31
|
-
|
|
32
|
-
const commandDefinitions: Record<string, CommandDefinition> = {
|
|
33
|
-
'workflow.start': {
|
|
34
|
-
description: 'Start one Workflow in a selected connected Tapicker Extension',
|
|
35
|
-
schema: WorkflowStartInputSchema,
|
|
36
|
-
execute: async (input, context, clientId) => {
|
|
37
|
-
const { clientId: selectedClientId, result } = await context.bridge.sendCommandWithClientId(
|
|
38
|
-
'workflow.start',
|
|
39
|
-
input,
|
|
40
|
-
clientId,
|
|
41
|
-
)
|
|
42
|
-
if (!result || typeof result !== 'object' || Array.isArray(result)) {
|
|
43
|
-
throw new BridgeError('InvalidExtensionResponse', 'Extension returned an invalid workflow.start response', {
|
|
44
|
-
status: 502,
|
|
45
|
-
})
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const parsed = WorkflowStartResultSchema.safeParse({ ...result, clientId: selectedClientId })
|
|
49
|
-
if (!parsed.success || parsed.data.workflowId !== (input as WorkflowStartInput).workflowId) {
|
|
50
|
-
throw new BridgeError('InvalidExtensionResponse', 'Extension returned an invalid workflow.start response', {
|
|
51
|
-
status: 502,
|
|
52
|
-
details: parsed.success
|
|
53
|
-
? {
|
|
54
|
-
requestedWorkflowId: (input as WorkflowStartInput).workflowId,
|
|
55
|
-
returnedWorkflowId: parsed.data.workflowId,
|
|
56
|
-
}
|
|
57
|
-
: parsed.error.issues,
|
|
58
|
-
})
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return parsed.data
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
'workflow.pause': {
|
|
65
|
-
description: 'Pause a running Workflow in a selected connected Tapicker Extension',
|
|
66
|
-
schema: WorkflowControlInputSchema,
|
|
67
|
-
execute: (input, context, clientId) => context.bridge.sendCommand('workflow.pause', input, clientId),
|
|
68
|
-
},
|
|
69
|
-
'workflow.resume': {
|
|
70
|
-
description: 'Resume a paused Workflow in a selected connected Tapicker Extension',
|
|
71
|
-
schema: WorkflowControlInputSchema,
|
|
72
|
-
execute: (input, context, clientId) => context.bridge.sendCommand('workflow.resume', input, clientId),
|
|
73
|
-
},
|
|
74
|
-
'workflow.stop': {
|
|
75
|
-
description: 'Stop a running Workflow in a selected connected Tapicker Extension',
|
|
76
|
-
schema: WorkflowControlInputSchema,
|
|
77
|
-
execute: (input, context, clientId) => context.bridge.sendCommand('workflow.stop', input, clientId),
|
|
78
|
-
},
|
|
79
|
-
'workflow.status': {
|
|
80
|
-
description: 'Get the current or most recently persisted Workflow status',
|
|
81
|
-
schema: WorkflowControlInputSchema,
|
|
82
|
-
execute: async (input, context, clientId) => {
|
|
83
|
-
const result = await context.bridge.sendCommand('workflow.status.get', input, clientId)
|
|
84
|
-
const parsed = WorkflowStatusDataSchema.safeParse(result)
|
|
85
|
-
if (!parsed.success || parsed.data.workflowId !== (input as WorkflowControlInput).workflowId) {
|
|
86
|
-
throw new BridgeError('InvalidExtensionResponse', 'Extension returned an invalid workflow status response', {
|
|
87
|
-
status: 502,
|
|
88
|
-
details: parsed.success ? undefined : parsed.error.issues,
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
return parsed.data
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
'data.get': {
|
|
95
|
-
description: 'Get an extracted data table by tableId; the rows count against the Extension export quota',
|
|
96
|
-
schema: DataInputSchema,
|
|
97
|
-
execute: (input, context, clientId) => context.bridge.sendCommand('data.get', input, clientId),
|
|
98
|
-
},
|
|
99
|
-
'data.delete': {
|
|
100
|
-
description: 'Delete an extracted data table by tableId',
|
|
101
|
-
schema: DataInputSchema,
|
|
102
|
-
execute: (input, context, clientId) => context.bridge.sendCommand('data.delete', input, clientId),
|
|
103
|
-
},
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function getCommandDefinition(name: string) {
|
|
107
|
-
return commandDefinitions[name]
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export async function dispatchCommand(name: string, rawInput: unknown, context: CommandContext) {
|
|
111
|
-
const definition = getCommandDefinition(name)
|
|
112
|
-
if (!definition) {
|
|
113
|
-
throw new BridgeError('CommandNotFound', `Unsupported command: ${name}`, { status: 404 })
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const request = CommandRequestSchema.safeParse(rawInput)
|
|
117
|
-
if (!request.success) {
|
|
118
|
-
throw new BridgeError('InvalidArguments', `Invalid command request for ${name}`, {
|
|
119
|
-
status: 400,
|
|
120
|
-
details: request.error.issues,
|
|
121
|
-
})
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const parsed = definition.schema.safeParse(request.data.input)
|
|
125
|
-
if (!parsed.success) {
|
|
126
|
-
throw new BridgeError('InvalidArguments', `Invalid arguments for ${name}`, {
|
|
127
|
-
status: 400,
|
|
128
|
-
details: parsed.error.issues,
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return definition.execute(parsed.data, context, request.data.clientId)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function getCommandCapabilities() {
|
|
136
|
-
return [
|
|
137
|
-
...Object.entries(commandDefinitions).map(([name, definition]) => ({
|
|
138
|
-
name,
|
|
139
|
-
description: definition.description,
|
|
140
|
-
inputSchema: z.toJSONSchema(
|
|
141
|
-
z
|
|
142
|
-
.object({
|
|
143
|
-
clientId: z.uuid().optional(),
|
|
144
|
-
input: definition.schema,
|
|
145
|
-
})
|
|
146
|
-
.strict(),
|
|
147
|
-
{ io: 'input' },
|
|
148
|
-
),
|
|
149
|
-
})),
|
|
150
|
-
{
|
|
151
|
-
name: 'workflow.watch',
|
|
152
|
-
description: 'Watch one Workflow through the workflow.watch WebSocket endpoint',
|
|
153
|
-
transport: 'websocket',
|
|
154
|
-
path: DEFAULT_WORKFLOW_WATCH_WS_PATH,
|
|
155
|
-
inputSchema: z.toJSONSchema(WorkflowWatchRequestSchema, { io: 'input' }),
|
|
156
|
-
},
|
|
157
|
-
]
|
|
158
|
-
}
|
package/src/constants.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export const VERSION = '1.0.0'
|
|
2
|
-
export const PROTOCOL_VERSION = 1
|
|
3
|
-
|
|
4
|
-
export const DEFAULT_HOST = '127.0.0.1'
|
|
5
|
-
export const DEFAULT_PORT = 9520
|
|
6
|
-
export const DEFAULT_PORT_MAX = 9530
|
|
7
|
-
export const DEFAULT_WS_PATH = '/ws'
|
|
8
|
-
export const DEFAULT_WORKFLOW_WATCH_WS_PATH = '/v1/commands/workflow.watch'
|
|
9
|
-
export const DEFAULT_MCP_PATH = '/mcp'
|
|
10
|
-
export const DEFAULT_REQUEST_TIMEOUT = 30_000
|
package/src/errors.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
export type BridgeErrorOptions = {
|
|
2
|
-
status?: number
|
|
3
|
-
details?: unknown
|
|
4
|
-
cause?: unknown
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export type SerializedBridgeError = {
|
|
8
|
-
code: string
|
|
9
|
-
message: string
|
|
10
|
-
details?: unknown
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class BridgeError extends Error {
|
|
14
|
-
readonly code: string
|
|
15
|
-
readonly status: number
|
|
16
|
-
readonly details: unknown
|
|
17
|
-
|
|
18
|
-
constructor(code: string, message: string, options: BridgeErrorOptions = {}) {
|
|
19
|
-
super(message, { cause: options.cause })
|
|
20
|
-
this.name = 'BridgeError'
|
|
21
|
-
this.code = code
|
|
22
|
-
this.status = options.status ?? 400
|
|
23
|
-
this.details = options.details
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function toBridgeError(error: unknown, fallbackCode = 'InternalError', fallbackStatus = 500) {
|
|
28
|
-
if (error instanceof BridgeError) {
|
|
29
|
-
return error
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (error instanceof Error) {
|
|
33
|
-
return new BridgeError(fallbackCode, error.message, {
|
|
34
|
-
status: fallbackStatus,
|
|
35
|
-
cause: error,
|
|
36
|
-
})
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return new BridgeError(fallbackCode, 'An unexpected error occurred', {
|
|
40
|
-
status: fallbackStatus,
|
|
41
|
-
details: error,
|
|
42
|
-
cause: error,
|
|
43
|
-
})
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function serializeBridgeError(error: unknown): SerializedBridgeError {
|
|
47
|
-
const bridgeError = toBridgeError(error)
|
|
48
|
-
const result: SerializedBridgeError = {
|
|
49
|
-
code: bridgeError.code,
|
|
50
|
-
message: bridgeError.message,
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (bridgeError.details !== undefined) {
|
|
54
|
-
result.details = bridgeError.details
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return result
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function safeError(error: unknown) {
|
|
61
|
-
return {
|
|
62
|
-
ok: false as const,
|
|
63
|
-
error: serializeBridgeError(error),
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function safeSuccess<T>(data: T) {
|
|
68
|
-
return {
|
|
69
|
-
ok: true as const,
|
|
70
|
-
data,
|
|
71
|
-
}
|
|
72
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
export { BridgeServer } from './server.js'
|
|
2
|
-
export { BridgeService } from './bridge/bridge-service.js'
|
|
3
|
-
export { ExtensionSession } from './bridge/extension-session.js'
|
|
4
|
-
export { dispatchCommand, getCommandCapabilities, getCommandDefinition } from './commands.js'
|
|
5
|
-
export { BridgeError, safeError, safeSuccess, serializeBridgeError } from './errors.js'
|
|
6
|
-
export {
|
|
7
|
-
BridgeHelloDataSchema,
|
|
8
|
-
BridgePairDataSchema,
|
|
9
|
-
DataInputSchema,
|
|
10
|
-
ExtensionHelloInputSchema,
|
|
11
|
-
ExtensionPairInputSchema,
|
|
12
|
-
MessageEnvelopeSchema,
|
|
13
|
-
SafeResultSchema,
|
|
14
|
-
TabTargetSchema,
|
|
15
|
-
WorkflowControlInputSchema,
|
|
16
|
-
WorkflowLifecycleStateSchema,
|
|
17
|
-
WorkflowStatusEventSchema,
|
|
18
|
-
WorkflowStatusDataSchema,
|
|
19
|
-
WorkflowStartInputSchema,
|
|
20
|
-
WorkflowStartResultSchema,
|
|
21
|
-
WorkflowWatchRequestSchema,
|
|
22
|
-
createEnvelope,
|
|
23
|
-
parseEnvelope,
|
|
24
|
-
} from './protocol/schema.js'
|
|
25
|
-
export type {
|
|
26
|
-
DataInput,
|
|
27
|
-
WorkflowControlInput,
|
|
28
|
-
WorkflowStartInput,
|
|
29
|
-
WorkflowStartResult,
|
|
30
|
-
WorkflowStatusData,
|
|
31
|
-
WorkflowStatusEvent,
|
|
32
|
-
WorkflowWatchRequest,
|
|
33
|
-
} from './protocol/schema.js'
|
|
34
|
-
export { connectWorkflowWatch, waitForWorkflowStatus, workflowWatchWebSocketUrl } from './workflow-watch.js'
|
|
35
|
-
export type { WorkflowWatchConnection, WorkflowWatchInput, WorkflowWatchResult } from './workflow-watch.js'
|
package/src/mcp.ts
DELETED
|
@@ -1,386 +0,0 @@
|
|
|
1
|
-
import { McpServer } from '@modelcontextprotocol/server'
|
|
2
|
-
import { z } from 'zod'
|
|
3
|
-
import { VERSION } from './constants.js'
|
|
4
|
-
import { dispatchCommand } from './commands.js'
|
|
5
|
-
import { BridgeError, safeError, safeSuccess } from './errors.js'
|
|
6
|
-
import type { BridgeService } from './bridge/bridge-service.js'
|
|
7
|
-
import { TabTargetSchema, WorkflowStatusEventSchema } from './protocol/schema.js'
|
|
8
|
-
import type { WorkflowStatusEvent } from './protocol/schema.js'
|
|
9
|
-
import { connectWorkflowWatch, waitForWorkflowStatus, workflowWatchWebSocketUrl } from './workflow-watch.js'
|
|
10
|
-
import type { WorkflowWatchInput, WorkflowWatchResult } from './workflow-watch.js'
|
|
11
|
-
|
|
12
|
-
export type TapickerMcpApi = {
|
|
13
|
-
executeCommand: (name: string, request: unknown) => Promise<unknown>
|
|
14
|
-
listExtensions: () => Promise<unknown[]>
|
|
15
|
-
watchWorkflow: (
|
|
16
|
-
input: WorkflowWatchInput,
|
|
17
|
-
onStatus: (event: WorkflowStatusEvent) => void,
|
|
18
|
-
signal?: AbortSignal,
|
|
19
|
-
) => Promise<WorkflowWatchResult>
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function createBridgeMcpApi(bridge: BridgeService): TapickerMcpApi {
|
|
23
|
-
return {
|
|
24
|
-
executeCommand: (name, request) => dispatchCommand(name, request, { bridge }),
|
|
25
|
-
listExtensions: async () => bridge.getExtensionStatuses(),
|
|
26
|
-
watchWorkflow: (input, onStatus, signal) => {
|
|
27
|
-
const clientId = bridge.resolveCommandClientId('workflow.status', input.clientId)
|
|
28
|
-
return waitForWorkflowStatus({
|
|
29
|
-
clientId,
|
|
30
|
-
workflowId: input.workflowId,
|
|
31
|
-
subscribe: (handleStatus, _handleClose) => {
|
|
32
|
-
const listener = (event: WorkflowStatusEvent) => {
|
|
33
|
-
if (event.clientId === clientId && event.workflowId === input.workflowId) handleStatus(event)
|
|
34
|
-
}
|
|
35
|
-
bridge.on('workflow.status', listener)
|
|
36
|
-
return () => bridge.off('workflow.status', listener)
|
|
37
|
-
},
|
|
38
|
-
readStatus: async () => {
|
|
39
|
-
const status = await bridge.sendCommand<unknown>(
|
|
40
|
-
'workflow.status.get',
|
|
41
|
-
{ workflowId: input.workflowId },
|
|
42
|
-
clientId,
|
|
43
|
-
)
|
|
44
|
-
return parseWorkflowStatusEvent(status, clientId, input.workflowId)
|
|
45
|
-
},
|
|
46
|
-
onStatus,
|
|
47
|
-
timeoutMs: input.timeoutMs,
|
|
48
|
-
signal,
|
|
49
|
-
})
|
|
50
|
-
},
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function createHttpMcpApi(baseUrl: string, token?: string): TapickerMcpApi {
|
|
55
|
-
const bridgeUrl = parseBridgeUrl(baseUrl)
|
|
56
|
-
const headers: Record<string, string> = {}
|
|
57
|
-
if (token) {
|
|
58
|
-
headers.Authorization = `Bearer ${token}`
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const executeCommand = async (name: string, request: unknown) => {
|
|
62
|
-
const response = await fetchBridge(new URL(`/v1/commands/${encodeURIComponent(name)}`, bridgeUrl), {
|
|
63
|
-
method: 'POST',
|
|
64
|
-
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
65
|
-
body: JSON.stringify(request),
|
|
66
|
-
})
|
|
67
|
-
const body = await readBridgeResponse(response)
|
|
68
|
-
|
|
69
|
-
if (!response.ok || !body.ok) {
|
|
70
|
-
throw new BridgeError(body.error?.code ?? 'BridgeRequestFailed', body.error?.message ?? response.statusText, {
|
|
71
|
-
status: response.status,
|
|
72
|
-
details: body.error?.details,
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return body.data
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
executeCommand,
|
|
81
|
-
listExtensions: async () => {
|
|
82
|
-
const response = await fetchBridge(new URL('/v1/extensions', bridgeUrl), { headers })
|
|
83
|
-
const body = await readBridgeResponse(response)
|
|
84
|
-
|
|
85
|
-
if (!response.ok || !Array.isArray(body.extensions)) {
|
|
86
|
-
throw new BridgeError(body.error?.code ?? 'BridgeRequestFailed', body.error?.message ?? response.statusText, {
|
|
87
|
-
status: response.status,
|
|
88
|
-
details: body.error?.details,
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return body.extensions
|
|
93
|
-
},
|
|
94
|
-
watchWorkflow: async (input, onStatus, signal) => {
|
|
95
|
-
const connection = await connectWorkflowWatch(
|
|
96
|
-
workflowWatchWebSocketUrl(bridgeUrl),
|
|
97
|
-
{ clientId: input.clientId, workflowId: input.workflowId },
|
|
98
|
-
token,
|
|
99
|
-
signal,
|
|
100
|
-
)
|
|
101
|
-
try {
|
|
102
|
-
return await waitForWorkflowStatus({
|
|
103
|
-
clientId: connection.clientId,
|
|
104
|
-
workflowId: input.workflowId,
|
|
105
|
-
subscribe: (handleStatus, handleClose) => {
|
|
106
|
-
const removeStatusListener = connection.onStatus(handleStatus)
|
|
107
|
-
const removeCloseListener = connection.onClose(handleClose)
|
|
108
|
-
return () => {
|
|
109
|
-
removeStatusListener()
|
|
110
|
-
removeCloseListener()
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
readStatus: async () => {
|
|
114
|
-
const status = await executeCommand('workflow.status', {
|
|
115
|
-
clientId: connection.clientId,
|
|
116
|
-
input: { workflowId: input.workflowId },
|
|
117
|
-
})
|
|
118
|
-
return parseWorkflowStatusEvent(status, connection.clientId, input.workflowId)
|
|
119
|
-
},
|
|
120
|
-
onStatus,
|
|
121
|
-
timeoutMs: input.timeoutMs,
|
|
122
|
-
signal,
|
|
123
|
-
})
|
|
124
|
-
} finally {
|
|
125
|
-
connection.close()
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function createTapickerMcpServer(api: TapickerMcpApi) {
|
|
132
|
-
const server = new McpServer({ name: 'tapicker-bridge', version: VERSION })
|
|
133
|
-
|
|
134
|
-
server.registerTool(
|
|
135
|
-
'extensions_list',
|
|
136
|
-
{
|
|
137
|
-
title: 'List connected Extensions',
|
|
138
|
-
description: 'List connected Tapicker Extensions and their clientId values for targeting Workflow commands.',
|
|
139
|
-
inputSchema: z.object({}).strict(),
|
|
140
|
-
annotations: { readOnlyHint: true },
|
|
141
|
-
},
|
|
142
|
-
async () => runTool(() => api.listExtensions()),
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
server.registerTool(
|
|
146
|
-
'workflow_start',
|
|
147
|
-
{
|
|
148
|
-
title: 'Start Workflow',
|
|
149
|
-
description:
|
|
150
|
-
'Start a Workflow in a connected Tapicker Extension. Returns the workflowId, full workflowName, and target clientId. If multiple Extensions are connected, provide clientId.',
|
|
151
|
-
inputSchema: z
|
|
152
|
-
.object({
|
|
153
|
-
workflowId: z.string().min(1).describe('The Workflow ID to start.'),
|
|
154
|
-
args: z.record(z.string(), z.unknown()).optional().describe('Optional Workflow input arguments.'),
|
|
155
|
-
tab: TabTargetSchema.optional().describe('Browser tab target; defaults to the active tab.'),
|
|
156
|
-
clientId: z
|
|
157
|
-
.uuid()
|
|
158
|
-
.optional()
|
|
159
|
-
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
160
|
-
})
|
|
161
|
-
.strict(),
|
|
162
|
-
annotations: { destructiveHint: true },
|
|
163
|
-
},
|
|
164
|
-
async ({ clientId, ...input }) => runTool(() => api.executeCommand('workflow.start', { clientId, input })),
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
for (const action of ['pause', 'resume', 'stop'] as const) {
|
|
168
|
-
server.registerTool(
|
|
169
|
-
`workflow_${action}`,
|
|
170
|
-
{
|
|
171
|
-
title: `${capitalize(action)} Workflow`,
|
|
172
|
-
description: `${capitalize(action)} a Workflow by workflowId. If multiple Extensions are connected, provide clientId.`,
|
|
173
|
-
inputSchema: z
|
|
174
|
-
.object({
|
|
175
|
-
workflowId: z.string().min(1).describe('The Workflow ID to control.'),
|
|
176
|
-
clientId: z
|
|
177
|
-
.uuid()
|
|
178
|
-
.optional()
|
|
179
|
-
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
180
|
-
})
|
|
181
|
-
.strict(),
|
|
182
|
-
annotations: action === 'stop' ? { destructiveHint: true } : undefined,
|
|
183
|
-
},
|
|
184
|
-
async ({ clientId, ...input }) => runTool(() => api.executeCommand(`workflow.${action}`, { clientId, input })),
|
|
185
|
-
)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
server.registerTool(
|
|
189
|
-
'workflow_status',
|
|
190
|
-
{
|
|
191
|
-
title: 'Get Workflow status',
|
|
192
|
-
description:
|
|
193
|
-
'Get the current or most recently persisted status for a Workflow. If multiple Extensions are connected, provide clientId.',
|
|
194
|
-
inputSchema: z
|
|
195
|
-
.object({
|
|
196
|
-
workflowId: z.string().min(1).describe('The Workflow ID to query.'),
|
|
197
|
-
clientId: z
|
|
198
|
-
.uuid()
|
|
199
|
-
.optional()
|
|
200
|
-
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
201
|
-
})
|
|
202
|
-
.strict(),
|
|
203
|
-
annotations: { readOnlyHint: true },
|
|
204
|
-
},
|
|
205
|
-
async ({ clientId, ...input }) => runTool(() => api.executeCommand('workflow.status', { clientId, input })),
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
server.registerTool(
|
|
209
|
-
'data_get',
|
|
210
|
-
{
|
|
211
|
-
title: 'Get extracted data',
|
|
212
|
-
description:
|
|
213
|
-
'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.',
|
|
214
|
-
inputSchema: z
|
|
215
|
-
.object({
|
|
216
|
-
tableId: z.string().trim().min(1).max(128).describe('The data table ID copied from Tapicker Data.'),
|
|
217
|
-
clientId: z
|
|
218
|
-
.uuid()
|
|
219
|
-
.optional()
|
|
220
|
-
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
221
|
-
})
|
|
222
|
-
.strict(),
|
|
223
|
-
},
|
|
224
|
-
async ({ clientId, tableId }) => runTool(() => api.executeCommand('data.get', { clientId, input: { tableId } })),
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
server.registerTool(
|
|
228
|
-
'data_delete',
|
|
229
|
-
{
|
|
230
|
-
title: 'Delete extracted data',
|
|
231
|
-
description:
|
|
232
|
-
'Delete an extracted data table and remove it from Tapicker Data by tableId. If multiple Extensions are connected, provide clientId.',
|
|
233
|
-
inputSchema: z
|
|
234
|
-
.object({
|
|
235
|
-
tableId: z.string().trim().min(1).max(128).describe('The data table ID copied from Tapicker Data.'),
|
|
236
|
-
clientId: z
|
|
237
|
-
.uuid()
|
|
238
|
-
.optional()
|
|
239
|
-
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
240
|
-
})
|
|
241
|
-
.strict(),
|
|
242
|
-
annotations: { destructiveHint: true },
|
|
243
|
-
},
|
|
244
|
-
async ({ clientId, tableId }) => runTool(() => api.executeCommand('data.delete', { clientId, input: { tableId } })),
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
server.registerTool(
|
|
248
|
-
'workflow_watch',
|
|
249
|
-
{
|
|
250
|
-
title: 'Wait for Workflow completion',
|
|
251
|
-
description:
|
|
252
|
-
'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.',
|
|
253
|
-
inputSchema: z
|
|
254
|
-
.object({
|
|
255
|
-
workflowId: z.string().min(1).describe('The Workflow ID to watch.'),
|
|
256
|
-
clientId: z
|
|
257
|
-
.uuid()
|
|
258
|
-
.optional()
|
|
259
|
-
.describe('Target Extension UUID; required when multiple Extensions are connected.'),
|
|
260
|
-
timeoutMs: z
|
|
261
|
-
.number()
|
|
262
|
-
.int()
|
|
263
|
-
.positive()
|
|
264
|
-
.max(2_147_483_647)
|
|
265
|
-
.optional()
|
|
266
|
-
.describe('Optional maximum time to wait in milliseconds.'),
|
|
267
|
-
})
|
|
268
|
-
.strict(),
|
|
269
|
-
annotations: { readOnlyHint: true },
|
|
270
|
-
},
|
|
271
|
-
async ({ clientId, workflowId, timeoutMs }, context) => {
|
|
272
|
-
const progressToken = context.mcpReq._meta?.progressToken
|
|
273
|
-
let progress = 0
|
|
274
|
-
let notificationQueue = Promise.resolve()
|
|
275
|
-
return runTool(async () => {
|
|
276
|
-
const result = await api.watchWorkflow(
|
|
277
|
-
{ clientId, workflowId, timeoutMs },
|
|
278
|
-
(event) => {
|
|
279
|
-
if (progressToken === undefined) return
|
|
280
|
-
progress += 1
|
|
281
|
-
notificationQueue = notificationQueue
|
|
282
|
-
.then(() =>
|
|
283
|
-
context.mcpReq.notify({
|
|
284
|
-
method: 'notifications/progress',
|
|
285
|
-
params: {
|
|
286
|
-
progressToken,
|
|
287
|
-
progress,
|
|
288
|
-
message: `Workflow status: ${event.state}`,
|
|
289
|
-
},
|
|
290
|
-
}),
|
|
291
|
-
)
|
|
292
|
-
.catch(() => undefined)
|
|
293
|
-
},
|
|
294
|
-
context.mcpReq.signal,
|
|
295
|
-
)
|
|
296
|
-
await notificationQueue
|
|
297
|
-
return result
|
|
298
|
-
})
|
|
299
|
-
},
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
return server
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function parseWorkflowStatusEvent(value: unknown, clientId: string, workflowId: string) {
|
|
306
|
-
const parsed = WorkflowStatusEventSchema.safeParse({ ...(value as object), clientId })
|
|
307
|
-
if (!parsed.success || parsed.data.workflowId !== workflowId) {
|
|
308
|
-
throw new BridgeError('InvalidExtensionResponse', 'Extension returned an invalid workflow status response', {
|
|
309
|
-
status: 502,
|
|
310
|
-
details: parsed.success ? undefined : parsed.error.issues,
|
|
311
|
-
})
|
|
312
|
-
}
|
|
313
|
-
return parsed.data
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function capitalize(value: string) {
|
|
317
|
-
return `${value[0].toUpperCase()}${value.slice(1)}`
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function parseBridgeUrl(value: string) {
|
|
321
|
-
let url: URL
|
|
322
|
-
try {
|
|
323
|
-
url = new URL(value)
|
|
324
|
-
} catch (cause) {
|
|
325
|
-
throw new BridgeError('InvalidBridgeUrl', 'Bridge URL must be a valid HTTP or HTTPS URL', { status: 400, cause })
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
329
|
-
throw new BridgeError('InvalidBridgeUrl', 'Bridge URL must use HTTP or HTTPS', { status: 400 })
|
|
330
|
-
}
|
|
331
|
-
if (url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
|
|
332
|
-
throw new BridgeError('InvalidBridgeUrl', 'Bridge URL must contain only the HTTP(S) origin and port', {
|
|
333
|
-
status: 400,
|
|
334
|
-
})
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return url
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
async function fetchBridge(url: URL, init: RequestInit) {
|
|
341
|
-
try {
|
|
342
|
-
return await fetch(url, { ...init, signal: AbortSignal.timeout(30_000) })
|
|
343
|
-
} catch (cause) {
|
|
344
|
-
throw new BridgeError('BridgeUnavailable', `Unable to reach Tapicker Bridge at ${url.origin}`, {
|
|
345
|
-
status: 503,
|
|
346
|
-
cause,
|
|
347
|
-
})
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
async function readBridgeResponse(response: Response) {
|
|
352
|
-
let body: {
|
|
353
|
-
ok?: boolean
|
|
354
|
-
data?: unknown
|
|
355
|
-
error?: { code?: string; message?: string; details?: unknown }
|
|
356
|
-
extensions?: unknown[]
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
try {
|
|
360
|
-
body = (await response.json()) as typeof body
|
|
361
|
-
} catch (cause) {
|
|
362
|
-
throw new BridgeError('InvalidBridgeResponse', 'Bridge returned an invalid JSON response', {
|
|
363
|
-
status: 502,
|
|
364
|
-
cause,
|
|
365
|
-
})
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (!body || typeof body !== 'object') {
|
|
369
|
-
throw new BridgeError('InvalidBridgeResponse', 'Bridge returned an invalid response', { status: 502 })
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
return body
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
async function runTool(operation: () => Promise<unknown>) {
|
|
376
|
-
try {
|
|
377
|
-
return {
|
|
378
|
-
content: [{ type: 'text' as const, text: JSON.stringify(safeSuccess(await operation()), null, 2) }],
|
|
379
|
-
}
|
|
380
|
-
} catch (error) {
|
|
381
|
-
return {
|
|
382
|
-
content: [{ type: 'text' as const, text: JSON.stringify(safeError(error), null, 2) }],
|
|
383
|
-
isError: true,
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|