caddie-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -0
- package/dist/client.d.ts +26 -0
- package/dist/client.js +134 -0
- package/dist/guide.d.ts +2 -0
- package/dist/guide.js +351 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +107 -0
- package/dist/setup.d.ts +9 -0
- package/dist/setup.js +429 -0
- package/dist/sse-client.d.ts +65 -0
- package/dist/sse-client.js +101 -0
- package/dist/tools/ask-widget-types.generated.d.ts +2 -0
- package/dist/tools/ask-widget-types.generated.js +34 -0
- package/dist/tools/blockchain.d.ts +2 -0
- package/dist/tools/blockchain.js +52 -0
- package/dist/tools/caddie.d.ts +2 -0
- package/dist/tools/caddie.js +148 -0
- package/dist/tools/catalog.d.ts +2 -0
- package/dist/tools/catalog.js +151 -0
- package/dist/tools/connectors.d.ts +2 -0
- package/dist/tools/connectors.js +200 -0
- package/dist/tools/database.d.ts +2 -0
- package/dist/tools/database.js +66 -0
- package/dist/tools/lookups.d.ts +2 -0
- package/dist/tools/lookups.js +282 -0
- package/dist/tools/org.d.ts +2 -0
- package/dist/tools/org.js +29 -0
- package/dist/tools/parse-caddie-blocks.d.ts +20 -0
- package/dist/tools/parse-caddie-blocks.js +305 -0
- package/dist/tools/runs.d.ts +2 -0
- package/dist/tools/runs.js +248 -0
- package/dist/tools/shared.d.ts +13 -0
- package/dist/tools/shared.js +27 -0
- package/dist/tools/workflows.d.ts +2 -0
- package/dist/tools/workflows.js +226 -0
- package/dist/types.d.ts +157 -0
- package/dist/types.js +2 -0
- package/package.json +37 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { request, parseJsonParam, truncateResponse } from "../client.js";
|
|
3
|
+
import { ACTION_TYPE_RE, definitionSchema, payloadSchema, validateWorkflowId, validateRunId, auditLog, } from "./shared.js";
|
|
4
|
+
const MAX_RESULT_BYTES = 50_000;
|
|
5
|
+
const TRUNCATION_SUFFIX = `Re-run with a smaller inputs.limit or add inputs.chainIds/inputs.filters to reduce results. ` +
|
|
6
|
+
`Use inputs.offset with nextOffset from the result to paginate.`;
|
|
7
|
+
function truncateResult(text) {
|
|
8
|
+
const buf = Buffer.from(text, "utf8");
|
|
9
|
+
if (buf.length <= MAX_RESULT_BYTES)
|
|
10
|
+
return text;
|
|
11
|
+
return (buf.subarray(0, MAX_RESULT_BYTES).toString("utf8") +
|
|
12
|
+
`\n...(truncated — ${buf.length} bytes total. ${TRUNCATION_SUFFIX})`);
|
|
13
|
+
}
|
|
14
|
+
export function registerRunTools(s) {
|
|
15
|
+
s.registerTool("b3os_run_workflow", {
|
|
16
|
+
description: `Trigger execution of a saved workflow. Returns a runId that you can track with
|
|
17
|
+
b3os_get_run. The workflow must be published (status: "active") to run.
|
|
18
|
+
|
|
19
|
+
Pass an optional payload for manual triggers that expect input data (e.g., a wallet
|
|
20
|
+
address, token symbol, or other parameters).`,
|
|
21
|
+
inputSchema: {
|
|
22
|
+
workflowId: z.string().describe("Workflow ID to run"),
|
|
23
|
+
payload: payloadSchema
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Optional trigger payload data (for manual triggers that expect input)"),
|
|
26
|
+
},
|
|
27
|
+
}, async ({ workflowId, payload }) => {
|
|
28
|
+
validateWorkflowId(workflowId);
|
|
29
|
+
auditLog("RUN", `workflow ${workflowId}`);
|
|
30
|
+
const body = {};
|
|
31
|
+
if (payload !== undefined)
|
|
32
|
+
body.payload = parseJsonParam(payload, "payload");
|
|
33
|
+
const result = await request(`/v1/workflows/${workflowId}/run`, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
body: Object.keys(body).length > 0 ? body : undefined,
|
|
36
|
+
});
|
|
37
|
+
if (!result?.runId)
|
|
38
|
+
throw new Error(`Failed to trigger workflow ${workflowId}`);
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: "text",
|
|
43
|
+
text: `Workflow triggered. Run ID: ${result.runId}\n\nUse b3os_get_run to check the result.`,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
s.registerTool("b3os_run_action", {
|
|
49
|
+
description: `Execute a single action immediately and return its result.
|
|
50
|
+
|
|
51
|
+
Simpler than b3os_run_ephemeral — no workflow definition needed.
|
|
52
|
+
Provide the action type and its inputs directly.
|
|
53
|
+
|
|
54
|
+
Use for one-shot lookups: "get ETH price", "fetch wallet balances",
|
|
55
|
+
"look up a token address". For multi-step workflows or if-node branching,
|
|
56
|
+
use b3os_run_ephemeral instead.
|
|
57
|
+
|
|
58
|
+
PAGINATION: Actions that return lists (e.g. wallet balances, market results) support
|
|
59
|
+
inputs.limit and inputs.offset. For wallet balance actions, always pass a limit (e.g.
|
|
60
|
+
limit: 20) and filter with chainIds or filters to avoid oversized responses. Use the
|
|
61
|
+
nextOffset field from the result to fetch the next page.
|
|
62
|
+
|
|
63
|
+
Action types use hyphens only (e.g. "coingecko-get-token-price", "sim-dune-get-wallet-balances").
|
|
64
|
+
Use b3os_search_actions to discover available action types.`,
|
|
65
|
+
inputSchema: {
|
|
66
|
+
actionType: z
|
|
67
|
+
.string()
|
|
68
|
+
.describe('Action type ID — hyphens only, no slashes (e.g. "coingecko-get-token-price", "sim-dune-get-wallet-balances")'),
|
|
69
|
+
inputs: z.any().optional().describe("Action input payload"),
|
|
70
|
+
connectorId: z.string().optional().describe("Connector ID for authenticated actions"),
|
|
71
|
+
},
|
|
72
|
+
}, async ({ actionType, inputs, connectorId }) => {
|
|
73
|
+
if (!ACTION_TYPE_RE.test(actionType)) {
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: `Invalid actionType format: "${actionType}". Use b3os_search_actions to find valid action types.`,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const body = {
|
|
84
|
+
inputs: inputs ? parseJsonParam(inputs, "inputs") : {},
|
|
85
|
+
};
|
|
86
|
+
if (connectorId)
|
|
87
|
+
body.connectorId = connectorId;
|
|
88
|
+
const result = await request(`/v1/actions/${actionType}/run`, { method: "POST", body, timeout: 35_000 });
|
|
89
|
+
if (!result)
|
|
90
|
+
throw new Error("Action returned empty response");
|
|
91
|
+
const text = truncateResult(JSON.stringify(result.result, null, 2));
|
|
92
|
+
return {
|
|
93
|
+
content: [{ type: "text", text: `${actionType} (${result.durationMs}ms):\n${text}` }],
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
s.registerTool("b3os_run_ephemeral", {
|
|
97
|
+
description: `Execute a workflow definition immediately without saving it. Runs synchronously —
|
|
98
|
+
blocks until all nodes complete and returns the full execution results.
|
|
99
|
+
|
|
100
|
+
Perfect for one-shot operations: "check ETH price", "get Polymarket leaderboard",
|
|
101
|
+
"fetch wallet balances". No polling needed — results come back in the response.
|
|
102
|
+
|
|
103
|
+
Constraints: max 20 nodes, manual trigger only, no wait/for-each nodes, 60s timeout.
|
|
104
|
+
|
|
105
|
+
The typical one-shot flow:
|
|
106
|
+
1. Construct a simple workflow definition (manual trigger → action)
|
|
107
|
+
2. b3os_run_ephemeral → executes and returns all node results
|
|
108
|
+
That's it — one call, nothing persisted.`,
|
|
109
|
+
inputSchema: {
|
|
110
|
+
definition: definitionSchema,
|
|
111
|
+
payload: payloadSchema.optional().describe("Optional trigger payload data"),
|
|
112
|
+
},
|
|
113
|
+
}, async ({ definition, payload }) => {
|
|
114
|
+
auditLog("RUN_EPHEMERAL");
|
|
115
|
+
const parsedDefinition = parseJsonParam(definition, "definition");
|
|
116
|
+
const body = { definition: parsedDefinition };
|
|
117
|
+
if (payload !== undefined)
|
|
118
|
+
body.payload = parseJsonParam(payload, "payload");
|
|
119
|
+
const result = await request("/v1/runs/sync", {
|
|
120
|
+
method: "POST",
|
|
121
|
+
body,
|
|
122
|
+
timeout: 65_000,
|
|
123
|
+
});
|
|
124
|
+
if (!result)
|
|
125
|
+
throw new Error("Sync run returned empty response");
|
|
126
|
+
const parts = [];
|
|
127
|
+
parts.push(`Run ${result.runId}: ${result.status} (${result.durationMs}ms)`);
|
|
128
|
+
if (result.executionState) {
|
|
129
|
+
for (const [nodeId, node] of Object.entries(result.executionState)) {
|
|
130
|
+
if (nodeId === "root")
|
|
131
|
+
continue;
|
|
132
|
+
if (node.status === "failure") {
|
|
133
|
+
parts.push(`\n--- FAILED: ${nodeId} (${node.type}) ---`);
|
|
134
|
+
if (node.result)
|
|
135
|
+
parts.push(truncateResult(JSON.stringify(node.result, null, 2)));
|
|
136
|
+
}
|
|
137
|
+
else if (node.status === "success" && node.result) {
|
|
138
|
+
parts.push(`\n--- ${nodeId} (${node.type}) ---`);
|
|
139
|
+
parts.push(truncateResult(JSON.stringify(node.result, null, 2)));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { content: [{ type: "text", text: truncateResponse(parts.join("\n")) }] };
|
|
144
|
+
});
|
|
145
|
+
s.registerTool("b3os_list_runs", {
|
|
146
|
+
description: `List workflow runs. Filter by workflowId and/or status.
|
|
147
|
+
Use status="failure" to find failed runs for debugging.`,
|
|
148
|
+
inputSchema: {
|
|
149
|
+
workflowId: z.string().optional().describe("Filter by workflow ID"),
|
|
150
|
+
status: z
|
|
151
|
+
.enum(["running", "success", "failure", "waiting", "cancelled"])
|
|
152
|
+
.optional()
|
|
153
|
+
.describe("Filter by run status (e.g. 'failure' to find failed runs)"),
|
|
154
|
+
limit: z.number().optional().describe("Max results (default: 20)"),
|
|
155
|
+
offset: z.number().optional().describe("Offset for pagination"),
|
|
156
|
+
},
|
|
157
|
+
}, async ({ workflowId, status, limit, offset, }) => {
|
|
158
|
+
const params = {};
|
|
159
|
+
if (workflowId)
|
|
160
|
+
params.workflowId = workflowId;
|
|
161
|
+
// The /v1/runs API doesn't support server-side status filtering,
|
|
162
|
+
// so we over-fetch and filter client-side when a status filter is requested.
|
|
163
|
+
const safeLimit = Math.min(limit || 20, 100);
|
|
164
|
+
if (status) {
|
|
165
|
+
params.limit = String(Math.max(safeLimit * 3, 60));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
params.limit = String(safeLimit);
|
|
169
|
+
}
|
|
170
|
+
if (offset && !status)
|
|
171
|
+
params.offset = String(offset);
|
|
172
|
+
const data = await request("/v1/runs", { params });
|
|
173
|
+
let runs = data?.items || [];
|
|
174
|
+
let hasMore = data?.hasMore ?? false;
|
|
175
|
+
if (status) {
|
|
176
|
+
runs = runs.filter(r => r.status === status);
|
|
177
|
+
if (runs.length > safeLimit) {
|
|
178
|
+
runs = runs.slice(0, safeLimit);
|
|
179
|
+
hasMore = true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
content: [
|
|
184
|
+
{
|
|
185
|
+
type: "text",
|
|
186
|
+
text: JSON.stringify({
|
|
187
|
+
runs: runs.map(r => ({
|
|
188
|
+
id: r.id,
|
|
189
|
+
workflowId: r.workflowId,
|
|
190
|
+
status: r.status,
|
|
191
|
+
triggerSource: r.triggerSource,
|
|
192
|
+
startedAt: r.startedAt,
|
|
193
|
+
finishedAt: r.finishedAt,
|
|
194
|
+
})),
|
|
195
|
+
hasMore,
|
|
196
|
+
}, null, 2),
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
s.registerTool("b3os_get_run", {
|
|
202
|
+
description: `Get details of a workflow run. By default returns full execution state for every node.
|
|
203
|
+
|
|
204
|
+
Use summary=true to get only run metadata (status, timing, IDs) without the heavy
|
|
205
|
+
execution state and workflow definition — much smaller payload, ideal for checking
|
|
206
|
+
run status or listing recent runs.
|
|
207
|
+
|
|
208
|
+
Use nodeIds to fetch execution state for specific nodes only — useful for debugging
|
|
209
|
+
a single failed node without downloading the entire state.
|
|
210
|
+
|
|
211
|
+
Check the failed node's input and result fields to understand what went wrong.`,
|
|
212
|
+
inputSchema: {
|
|
213
|
+
runId: z.string().describe("Run ID (e.g. 'run_abc123')"),
|
|
214
|
+
summary: z
|
|
215
|
+
.boolean()
|
|
216
|
+
.optional()
|
|
217
|
+
.describe("When true, omit ExecutionState and WorkflowDefinition to reduce payload size"),
|
|
218
|
+
nodeIds: z
|
|
219
|
+
.array(z.string())
|
|
220
|
+
.optional()
|
|
221
|
+
.describe("Only include these node IDs in ExecutionState (comma-separated in the API)"),
|
|
222
|
+
},
|
|
223
|
+
}, async ({ runId, summary, nodeIds }) => {
|
|
224
|
+
validateRunId(runId);
|
|
225
|
+
const params = {};
|
|
226
|
+
if (summary)
|
|
227
|
+
params.summary = "true";
|
|
228
|
+
if (nodeIds?.length)
|
|
229
|
+
params.nodeIds = nodeIds.join(",");
|
|
230
|
+
const run = await request(`/v1/runs/${runId}`, { params });
|
|
231
|
+
if (!run)
|
|
232
|
+
throw new Error(`Run ${runId} not found`);
|
|
233
|
+
return { content: [{ type: "text", text: truncateResponse(JSON.stringify(run, null, 2)) }] };
|
|
234
|
+
});
|
|
235
|
+
s.registerTool("b3os_cancel_run", {
|
|
236
|
+
description: `Cancel a running workflow execution. Use this to stop a workflow that is stuck,
|
|
237
|
+
misconfigured, or performing unwanted actions. Only works on runs with status "running"
|
|
238
|
+
or "waiting".`,
|
|
239
|
+
inputSchema: {
|
|
240
|
+
runId: z.string().describe("Run ID to cancel (e.g. 'run_abc123')"),
|
|
241
|
+
},
|
|
242
|
+
}, async ({ runId }) => {
|
|
243
|
+
validateRunId(runId);
|
|
244
|
+
auditLog("CANCEL", `run ${runId}`);
|
|
245
|
+
await request(`/v1/runs/${runId}/cancel`, { method: "POST" });
|
|
246
|
+
return { content: [{ type: "text", text: `Run ${runId} cancelled.` }] };
|
|
247
|
+
});
|
|
248
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Regex for action/trigger type identifiers (lowercase slug format). */
|
|
3
|
+
export declare const ACTION_TYPE_RE: RegExp;
|
|
4
|
+
/** Workflow definition schema — must be an object with a `nodes` map. */
|
|
5
|
+
export declare const definitionSchema: z.ZodObject<{
|
|
6
|
+
nodes: z.ZodRecord<z.ZodString, z.ZodAny>;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
/** Payload schema — must be a key-value object, not a primitive or array. */
|
|
9
|
+
export declare const payloadSchema: z.ZodRecord<z.ZodString, z.ZodAny>;
|
|
10
|
+
export declare function validateWorkflowId(workflowId: string): void;
|
|
11
|
+
export declare function validateRunId(runId: string): void;
|
|
12
|
+
export declare function validateOrgId(orgId: string): void;
|
|
13
|
+
export declare function auditLog(action: string, detail?: string): void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Regex for action/trigger type identifiers (lowercase slug format). */
|
|
3
|
+
export const ACTION_TYPE_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
4
|
+
/** Workflow definition schema — must be an object with a `nodes` map. */
|
|
5
|
+
export const definitionSchema = z
|
|
6
|
+
.object({ nodes: z.record(z.string(), z.any()) })
|
|
7
|
+
.describe("Workflow definition (JSON object with nodes map)");
|
|
8
|
+
/** Payload schema — must be a key-value object, not a primitive or array. */
|
|
9
|
+
export const payloadSchema = z.record(z.string(), z.any()).describe("Payload key-value object");
|
|
10
|
+
export function validateWorkflowId(workflowId) {
|
|
11
|
+
if (!/^wf_\w+$/.test(workflowId)) {
|
|
12
|
+
throw new Error("Invalid workflowId format — expected 'wf_' prefix");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function validateRunId(runId) {
|
|
16
|
+
if (!/^run_\w+$/.test(runId)) {
|
|
17
|
+
throw new Error("Invalid runId format — expected 'run_' prefix");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function validateOrgId(orgId) {
|
|
21
|
+
if (!/^org_\w+$/.test(orgId)) {
|
|
22
|
+
throw new Error("Invalid orgId format — expected 'org_' prefix (e.g. 'org_abc123')");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function auditLog(action, detail) {
|
|
26
|
+
console.error(`[b3os-mcp] ${action}${detail ? ` ${detail}` : ""} at ${new Date().toISOString()}`);
|
|
27
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { request, parseJsonParam, truncateResponse } from "../client.js";
|
|
3
|
+
import { definitionSchema, validateWorkflowId, auditLog } from "./shared.js";
|
|
4
|
+
const WEB_APP_URL = "https://b3os.org";
|
|
5
|
+
function workflowEditUrl(workflowId) {
|
|
6
|
+
return `${WEB_APP_URL}/workflows/edit?id=${workflowId}`;
|
|
7
|
+
}
|
|
8
|
+
export function registerWorkflowTools(s) {
|
|
9
|
+
s.registerTool("b3os_list_workflows", {
|
|
10
|
+
description: `List workflows in the organization. Returns workflow metadata (id, name, status,
|
|
11
|
+
last triggered). Use this to find a workflow by name before getting its details.
|
|
12
|
+
Filter by status to find active, paused, or draft workflows.`,
|
|
13
|
+
inputSchema: {
|
|
14
|
+
status: z.enum(["draft", "active", "paused", "archived"]).optional().describe("Filter by workflow status"),
|
|
15
|
+
limit: z.number().optional().describe("Max results (default: 20)"),
|
|
16
|
+
offset: z.number().optional().describe("Offset for pagination"),
|
|
17
|
+
},
|
|
18
|
+
}, async ({ status, limit, offset }) => {
|
|
19
|
+
const params = {
|
|
20
|
+
_fields_filter: "id,name,status,description,lastTriggeredAt,createdAt",
|
|
21
|
+
};
|
|
22
|
+
// The /v1/workflows API doesn't support server-side status filtering,
|
|
23
|
+
// so we over-fetch and filter client-side when a status filter is requested.
|
|
24
|
+
const safeLimit = Math.min(limit || 20, 100);
|
|
25
|
+
if (status) {
|
|
26
|
+
params.limit = String(Math.max(safeLimit * 3, 60));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
params.limit = String(safeLimit);
|
|
30
|
+
}
|
|
31
|
+
if (offset && !status)
|
|
32
|
+
params.offset = String(offset);
|
|
33
|
+
const data = await request("/v1/workflows", { params });
|
|
34
|
+
let workflows = data?.items || [];
|
|
35
|
+
let hasMore = data?.hasMore ?? false;
|
|
36
|
+
if (status) {
|
|
37
|
+
workflows = workflows.filter(w => w.status === status);
|
|
38
|
+
if (workflows.length > safeLimit) {
|
|
39
|
+
workflows = workflows.slice(0, safeLimit);
|
|
40
|
+
hasMore = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: "text",
|
|
47
|
+
text: JSON.stringify({
|
|
48
|
+
workflows: workflows.map(w => ({
|
|
49
|
+
id: w.id,
|
|
50
|
+
name: w.name,
|
|
51
|
+
status: w.status,
|
|
52
|
+
description: w.description,
|
|
53
|
+
lastTriggeredAt: w.lastTriggeredAt,
|
|
54
|
+
createdAt: w.createdAt,
|
|
55
|
+
})),
|
|
56
|
+
hasMore,
|
|
57
|
+
}, null, 2),
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
s.registerTool("b3os_get_workflow", {
|
|
63
|
+
description: `Get a workflow's full details including its definition (nodes, triggers, connections).
|
|
64
|
+
Use this to inspect an existing workflow before modifying it.`,
|
|
65
|
+
inputSchema: { workflowId: z.string().describe("Workflow ID (e.g. 'wf_abc123')") },
|
|
66
|
+
}, async ({ workflowId }) => {
|
|
67
|
+
validateWorkflowId(workflowId);
|
|
68
|
+
const workflow = await request(`/v1/workflows/${workflowId}`);
|
|
69
|
+
if (!workflow)
|
|
70
|
+
throw new Error(`Workflow ${workflowId} not found`);
|
|
71
|
+
return { content: [{ type: "text", text: truncateResponse(JSON.stringify(workflow, null, 2)) }] };
|
|
72
|
+
});
|
|
73
|
+
s.registerTool("b3os_create_workflow", {
|
|
74
|
+
description: `Save a new workflow to B3OS.
|
|
75
|
+
|
|
76
|
+
IMPORTANT: Always call b3os_validate_workflow first to catch errors before saving.
|
|
77
|
+
After saving, use b3os_publish_workflow to make it live.`,
|
|
78
|
+
inputSchema: {
|
|
79
|
+
name: z.string().describe("Workflow name"),
|
|
80
|
+
description: z.string().describe("What this workflow does"),
|
|
81
|
+
definition: definitionSchema,
|
|
82
|
+
},
|
|
83
|
+
}, async ({ name, description, definition }) => {
|
|
84
|
+
// Some MCP clients pass definition as a JSON string instead of an object — parse it to avoid double-serialization
|
|
85
|
+
const parsedDefinition = parseJsonParam(definition, "definition");
|
|
86
|
+
const workflow = await request("/v1/workflows", {
|
|
87
|
+
method: "POST",
|
|
88
|
+
body: { name, description, definition: parsedDefinition },
|
|
89
|
+
});
|
|
90
|
+
if (!workflow)
|
|
91
|
+
throw new Error("Failed to create workflow — empty response");
|
|
92
|
+
const url = workflowEditUrl(workflow.id);
|
|
93
|
+
return {
|
|
94
|
+
content: [
|
|
95
|
+
{
|
|
96
|
+
type: "text",
|
|
97
|
+
text: `Workflow created successfully.\n\n${JSON.stringify({ id: workflow.id, name: workflow.name, status: workflow.status, version: workflow.version }, null, 2)}\n\nLink: ${url}\nNext: use b3os_publish_workflow to make it live.`,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
s.registerTool("b3os_update_workflow", {
|
|
103
|
+
description: `Update an existing workflow's metadata or definition. Only include the fields you want to change.`,
|
|
104
|
+
inputSchema: {
|
|
105
|
+
workflowId: z.string().describe("Workflow ID to update"),
|
|
106
|
+
name: z.string().optional().describe("New name"),
|
|
107
|
+
description: z.string().optional().describe("New description"),
|
|
108
|
+
definition: definitionSchema.optional(),
|
|
109
|
+
expectedCurrentVersion: z.number().optional().describe("Expected current version (optimistic locking)"),
|
|
110
|
+
},
|
|
111
|
+
}, async ({ workflowId, name, description, definition, expectedCurrentVersion, }) => {
|
|
112
|
+
validateWorkflowId(workflowId);
|
|
113
|
+
const body = {};
|
|
114
|
+
if (name !== undefined)
|
|
115
|
+
body.name = name;
|
|
116
|
+
if (description !== undefined)
|
|
117
|
+
body.description = description;
|
|
118
|
+
if (definition !== undefined)
|
|
119
|
+
body.definition = parseJsonParam(definition, "definition");
|
|
120
|
+
if (expectedCurrentVersion !== undefined)
|
|
121
|
+
body.expectedCurrentVersion = expectedCurrentVersion;
|
|
122
|
+
const workflow = await request(`/v1/workflows/${workflowId}`, {
|
|
123
|
+
method: "PUT",
|
|
124
|
+
body,
|
|
125
|
+
});
|
|
126
|
+
if (!workflow)
|
|
127
|
+
throw new Error(`Workflow ${workflowId} not found`);
|
|
128
|
+
const url = workflowEditUrl(workflow.id);
|
|
129
|
+
return {
|
|
130
|
+
content: [
|
|
131
|
+
{
|
|
132
|
+
type: "text",
|
|
133
|
+
text: `Workflow updated.\n\n${JSON.stringify({ id: workflow.id, name: workflow.name, status: workflow.status, version: workflow.version }, null, 2)}\n\nLink: ${url}`,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
s.registerTool("b3os_delete_workflow", {
|
|
139
|
+
description: `Delete (archive) a workflow. This is irreversible. The workflow will be archived
|
|
140
|
+
and no longer execute. Always confirm with the user before calling this tool.`,
|
|
141
|
+
inputSchema: {
|
|
142
|
+
workflowId: z.string().describe("Workflow ID to delete"),
|
|
143
|
+
confirm: z.literal(true).describe("Must be exactly true to confirm deletion — this is irreversible"),
|
|
144
|
+
},
|
|
145
|
+
}, async ({ workflowId }) => {
|
|
146
|
+
validateWorkflowId(workflowId);
|
|
147
|
+
auditLog("DELETE", `workflow ${workflowId}`);
|
|
148
|
+
await request(`/v1/workflows/${workflowId}`, { method: "DELETE" });
|
|
149
|
+
return { content: [{ type: "text", text: `Workflow ${workflowId} deleted.` }] };
|
|
150
|
+
});
|
|
151
|
+
s.registerTool("b3os_publish_workflow", {
|
|
152
|
+
description: `Publish a draft workflow to make it live. After creating or updating a workflow,
|
|
153
|
+
it starts in "draft" status. Publishing activates its triggers (schedules, webhooks, etc.)
|
|
154
|
+
so it begins executing automatically.`,
|
|
155
|
+
inputSchema: {
|
|
156
|
+
workflowId: z.string().describe("Workflow ID to publish"),
|
|
157
|
+
expectedVersion: z.number().optional().describe("Expected version (optimistic locking)"),
|
|
158
|
+
},
|
|
159
|
+
}, async ({ workflowId, expectedVersion }) => {
|
|
160
|
+
validateWorkflowId(workflowId);
|
|
161
|
+
auditLog("PUBLISH", `workflow ${workflowId}`);
|
|
162
|
+
const body = {};
|
|
163
|
+
if (expectedVersion !== undefined)
|
|
164
|
+
body.expectedVersion = expectedVersion;
|
|
165
|
+
const workflow = await request(`/v1/workflows/${workflowId}/publish`, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
body: Object.keys(body).length > 0 ? body : undefined,
|
|
168
|
+
});
|
|
169
|
+
if (!workflow)
|
|
170
|
+
throw new Error(`Failed to publish workflow ${workflowId}`);
|
|
171
|
+
const url = workflowEditUrl(workflowId);
|
|
172
|
+
return {
|
|
173
|
+
content: [
|
|
174
|
+
{
|
|
175
|
+
type: "text",
|
|
176
|
+
text: workflow.name
|
|
177
|
+
? `Workflow "${workflow.name}" is now live (status: ${workflow.status}).\n\nLink: ${url}`
|
|
178
|
+
: `Workflow ${workflowId} published successfully (status: ${workflow.status || "active"}).\n\nLink: ${url}`,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
s.registerTool("b3os_pause_workflow", {
|
|
184
|
+
description: `Pause an active workflow. Its triggers will stop firing and no new runs will start.
|
|
185
|
+
Use b3os_resume_workflow to reactivate it.`,
|
|
186
|
+
inputSchema: { workflowId: z.string().describe("Workflow ID to pause") },
|
|
187
|
+
}, async ({ workflowId }) => {
|
|
188
|
+
validateWorkflowId(workflowId);
|
|
189
|
+
await request(`/v1/workflows/${workflowId}/pause`, { method: "POST" });
|
|
190
|
+
return { content: [{ type: "text", text: `Workflow ${workflowId} paused.` }] };
|
|
191
|
+
});
|
|
192
|
+
s.registerTool("b3os_resume_workflow", {
|
|
193
|
+
description: `Resume a paused workflow. Its triggers will start firing again.`,
|
|
194
|
+
inputSchema: { workflowId: z.string().describe("Workflow ID to resume") },
|
|
195
|
+
}, async ({ workflowId }) => {
|
|
196
|
+
validateWorkflowId(workflowId);
|
|
197
|
+
await request(`/v1/workflows/${workflowId}/resume`, { method: "POST" });
|
|
198
|
+
return { content: [{ type: "text", text: `Workflow ${workflowId} resumed.` }] };
|
|
199
|
+
});
|
|
200
|
+
s.registerTool("b3os_validate_workflow", {
|
|
201
|
+
description: `Validate a workflow definition without saving it. ALWAYS call this before
|
|
202
|
+
b3os_create_workflow or b3os_update_workflow. No side effects. Returns validation results
|
|
203
|
+
including any missing fields, invalid payloads, or misconfigured nodes.`,
|
|
204
|
+
inputSchema: {
|
|
205
|
+
name: z
|
|
206
|
+
.string()
|
|
207
|
+
.optional()
|
|
208
|
+
.describe("Workflow name (defaults to 'Untitled' — the API requires a name for validation)"),
|
|
209
|
+
definition: definitionSchema,
|
|
210
|
+
},
|
|
211
|
+
}, async ({ name, definition }) => {
|
|
212
|
+
const parsedDefinition = parseJsonParam(definition, "definition");
|
|
213
|
+
const result = await request("/v1/workflows/validate", {
|
|
214
|
+
method: "POST",
|
|
215
|
+
body: { name: name || "Untitled", definition: parsedDefinition },
|
|
216
|
+
});
|
|
217
|
+
return {
|
|
218
|
+
content: [
|
|
219
|
+
{
|
|
220
|
+
type: "text",
|
|
221
|
+
text: result ? `Validation result:\n${JSON.stringify(result, null, 2)}` : "Workflow definition is valid.",
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
export interface ApiResponse<T> {
|
|
2
|
+
code: number;
|
|
3
|
+
message: string;
|
|
4
|
+
data: T;
|
|
5
|
+
}
|
|
6
|
+
export interface PaginatedData<T> {
|
|
7
|
+
items: T[];
|
|
8
|
+
limit: number;
|
|
9
|
+
offset: number;
|
|
10
|
+
hasMore: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface Organization {
|
|
13
|
+
id: string;
|
|
14
|
+
slug: string;
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
photo?: string;
|
|
18
|
+
createdBy: string;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
updatedAt: string;
|
|
21
|
+
}
|
|
22
|
+
export interface Wallet {
|
|
23
|
+
id: string;
|
|
24
|
+
organizationId: string;
|
|
25
|
+
address: string;
|
|
26
|
+
name: string;
|
|
27
|
+
isDefault: boolean;
|
|
28
|
+
createdAt: number;
|
|
29
|
+
updatedAt: number;
|
|
30
|
+
}
|
|
31
|
+
export interface Connector {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
type: string;
|
|
35
|
+
provider?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface Workflow {
|
|
38
|
+
id: string;
|
|
39
|
+
version: number;
|
|
40
|
+
organizationId: string;
|
|
41
|
+
name: string;
|
|
42
|
+
description: string;
|
|
43
|
+
status: "draft" | "active" | "paused" | "archived";
|
|
44
|
+
type: string;
|
|
45
|
+
definition: WorkflowDefinition;
|
|
46
|
+
maxRuns: number;
|
|
47
|
+
remainRuns: number;
|
|
48
|
+
cooldownMs: number;
|
|
49
|
+
lastTriggeredAt?: string;
|
|
50
|
+
createdBy?: string;
|
|
51
|
+
createdAt: string;
|
|
52
|
+
updatedBy?: string;
|
|
53
|
+
updatedAt: string;
|
|
54
|
+
hasDraft?: boolean;
|
|
55
|
+
hasLiveVersion?: boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface WorkflowDefinition {
|
|
58
|
+
nodes: Record<string, WorkflowNode>;
|
|
59
|
+
}
|
|
60
|
+
export interface WorkflowNode {
|
|
61
|
+
type: string;
|
|
62
|
+
description?: string;
|
|
63
|
+
connector?: {
|
|
64
|
+
type: string;
|
|
65
|
+
id?: string;
|
|
66
|
+
};
|
|
67
|
+
payload: Record<string, unknown>;
|
|
68
|
+
children: string[];
|
|
69
|
+
branch?: string;
|
|
70
|
+
}
|
|
71
|
+
export interface ExecutionNode {
|
|
72
|
+
type: string;
|
|
73
|
+
status: "pending" | "running" | "success" | "failure" | "skipped" | "waiting";
|
|
74
|
+
input?: Record<string, unknown>;
|
|
75
|
+
result?: Record<string, unknown>;
|
|
76
|
+
startedAt?: string;
|
|
77
|
+
finishedAt?: string;
|
|
78
|
+
}
|
|
79
|
+
export interface Run {
|
|
80
|
+
id: string;
|
|
81
|
+
organizationId: string;
|
|
82
|
+
workflowId: string;
|
|
83
|
+
workflowVersion: number;
|
|
84
|
+
status: "running" | "success" | "failure" | "waiting" | "cancelled";
|
|
85
|
+
executionState: Record<string, ExecutionNode>;
|
|
86
|
+
triggerSource?: string;
|
|
87
|
+
triggeredBy?: string;
|
|
88
|
+
startedAt?: string;
|
|
89
|
+
finishedAt?: string;
|
|
90
|
+
}
|
|
91
|
+
export interface TelegramChat {
|
|
92
|
+
id: string;
|
|
93
|
+
chatId: number;
|
|
94
|
+
chatType: string;
|
|
95
|
+
chatTitle?: string;
|
|
96
|
+
chatUsername?: string;
|
|
97
|
+
connectorId?: string;
|
|
98
|
+
connectorName?: string;
|
|
99
|
+
verifiedAt: string;
|
|
100
|
+
}
|
|
101
|
+
export interface SlackChannel {
|
|
102
|
+
id: string;
|
|
103
|
+
name: string;
|
|
104
|
+
is_private?: boolean;
|
|
105
|
+
is_archived?: boolean;
|
|
106
|
+
num_members?: number;
|
|
107
|
+
}
|
|
108
|
+
export interface ActionTestResult {
|
|
109
|
+
result?: {
|
|
110
|
+
data?: {
|
|
111
|
+
ret?: Record<string, unknown>;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export interface ActionDefinition {
|
|
116
|
+
type: string;
|
|
117
|
+
name: string;
|
|
118
|
+
description: string;
|
|
119
|
+
payloadSchema: Record<string, unknown>;
|
|
120
|
+
resultSchema: Record<string, unknown>;
|
|
121
|
+
logoUrl: string;
|
|
122
|
+
createdBy: string;
|
|
123
|
+
documentationUrl: string;
|
|
124
|
+
connector?: {
|
|
125
|
+
type: string;
|
|
126
|
+
};
|
|
127
|
+
category: string;
|
|
128
|
+
tags: string[];
|
|
129
|
+
requiresGas: boolean;
|
|
130
|
+
}
|
|
131
|
+
export interface TriggerDefinition {
|
|
132
|
+
type: string;
|
|
133
|
+
name: string;
|
|
134
|
+
description: string;
|
|
135
|
+
payloadSchema: Record<string, unknown>;
|
|
136
|
+
resultSchema: Record<string, unknown>;
|
|
137
|
+
logoUrl: string;
|
|
138
|
+
createdBy: string;
|
|
139
|
+
documentationUrl: string;
|
|
140
|
+
connector?: {
|
|
141
|
+
type: string;
|
|
142
|
+
};
|
|
143
|
+
tags: string[];
|
|
144
|
+
}
|
|
145
|
+
export interface SearchHit {
|
|
146
|
+
id: string;
|
|
147
|
+
type: string;
|
|
148
|
+
name: string;
|
|
149
|
+
description: string;
|
|
150
|
+
category?: string;
|
|
151
|
+
tags?: string[];
|
|
152
|
+
score: number;
|
|
153
|
+
}
|
|
154
|
+
export interface SearchResults {
|
|
155
|
+
results: SearchHit[];
|
|
156
|
+
totalHits: number;
|
|
157
|
+
}
|
package/dist/types.js
ADDED