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,148 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { request, parseJsonParam, truncateResponse, getApiKey, getServerUrl } from "../client.js";
|
|
3
|
+
import { consumeCaddieStream } from "../sse-client.js";
|
|
4
|
+
import { definitionSchema, validateWorkflowId, validateRunId, auditLog } from "./shared.js";
|
|
5
|
+
import { formatCaddieBlocksForCLI } from "./parse-caddie-blocks.js";
|
|
6
|
+
/** Format a Caddie workflow response with incomplete fields, validation errors, and next steps. */
|
|
7
|
+
function formatWorkflowResponse(done, header, nextSteps) {
|
|
8
|
+
const parts = [header];
|
|
9
|
+
parts.push(JSON.stringify(done.data.workflow, null, 2));
|
|
10
|
+
if (done.data.incompleteFields?.length) {
|
|
11
|
+
parts.push(`\nIncomplete fields that need user input:\n- ${done.data.incompleteFields.join("\n- ")}`);
|
|
12
|
+
}
|
|
13
|
+
if (done.data.validationErrors?.length) {
|
|
14
|
+
parts.push("\nValidation errors:");
|
|
15
|
+
for (const err of done.data.validationErrors) {
|
|
16
|
+
parts.push(`- ${err.nodeId ? `[${err.nodeId}] ` : ""}${err.message}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
parts.push(`\nNext steps:\n${nextSteps}`);
|
|
20
|
+
return truncateResponse(parts.join("\n"));
|
|
21
|
+
}
|
|
22
|
+
export function registerCaddieTools(s) {
|
|
23
|
+
s.registerTool("b3os_build_workflow", {
|
|
24
|
+
description: `Build or modify a workflow using Caddie, the B3OS AI agent. Describe what you want
|
|
25
|
+
in natural language and Caddie will generate or update the workflow definition.
|
|
26
|
+
|
|
27
|
+
Use this when you need to:
|
|
28
|
+
- Create a new workflow from a description ("monitor ETH price and alert on Telegram")
|
|
29
|
+
- Modify an existing workflow ("add a filter node before the swap action")
|
|
30
|
+
- Get help designing complex logic (branching, loops, error handling)
|
|
31
|
+
|
|
32
|
+
If workflowId is provided, Caddie loads the existing workflow and modifies it.
|
|
33
|
+
If definition is provided, Caddie uses it as the starting point.
|
|
34
|
+
Otherwise, Caddie creates a new workflow from scratch.
|
|
35
|
+
|
|
36
|
+
Returns the workflow definition JSON (ready for b3os_create_workflow or b3os_update_workflow)
|
|
37
|
+
along with any incomplete fields that still need user input and validation errors.`,
|
|
38
|
+
inputSchema: {
|
|
39
|
+
message: z.string().describe("Natural language description of what to build or change"),
|
|
40
|
+
workflowId: z.string().optional().describe("Existing workflow ID to modify (e.g. 'wf_abc123')"),
|
|
41
|
+
definition: definitionSchema.optional().describe("Starting definition to modify (alternative to workflowId)"),
|
|
42
|
+
},
|
|
43
|
+
}, async ({ message, workflowId, definition }) => {
|
|
44
|
+
try {
|
|
45
|
+
if (workflowId)
|
|
46
|
+
validateWorkflowId(workflowId);
|
|
47
|
+
auditLog("CADDIE_BUILD", workflowId ? `workflow ${workflowId}` : "new workflow");
|
|
48
|
+
const apiKey = getApiKey();
|
|
49
|
+
const serverUrl = getServerUrl();
|
|
50
|
+
const parsedDefinition = definition
|
|
51
|
+
? parseJsonParam(definition, "definition")
|
|
52
|
+
: undefined;
|
|
53
|
+
const done = await consumeCaddieStream(apiKey, serverUrl, {
|
|
54
|
+
message,
|
|
55
|
+
workflowId,
|
|
56
|
+
definition: parsedDefinition,
|
|
57
|
+
});
|
|
58
|
+
if (done.data.type === "workflow" && done.data.workflow) {
|
|
59
|
+
const text = formatWorkflowResponse(done, "Caddie generated a workflow definition:\n", "1. Review the definition and fill in any incomplete fields\n2. Use b3os_validate_workflow to check for errors\n3. Use b3os_create_workflow or b3os_update_workflow to save it");
|
|
60
|
+
return { content: [{ type: "text", text }] };
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
content: [
|
|
64
|
+
{
|
|
65
|
+
type: "text",
|
|
66
|
+
text: formatCaddieBlocksForCLI(done.data.message || "Caddie did not return a workflow or message."),
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
return {
|
|
73
|
+
content: [{ type: "text", text: `Caddie build failed: ${err instanceof Error ? err.message : err}` }],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
s.registerTool("b3os_debug_run", {
|
|
78
|
+
description: `Debug a failed or stuck workflow run using Caddie, the B3OS AI agent.
|
|
79
|
+
Provide a run ID and Caddie will analyze the execution state, identify what went wrong,
|
|
80
|
+
and suggest fixes — potentially returning a corrected workflow definition.
|
|
81
|
+
|
|
82
|
+
Use this when:
|
|
83
|
+
- A run failed and you want to understand why
|
|
84
|
+
- A run is stuck in "running" or "waiting" status
|
|
85
|
+
- You need help fixing a workflow based on runtime errors
|
|
86
|
+
|
|
87
|
+
Caddie receives the full run context (status, timing, execution state per node)
|
|
88
|
+
and provides a diagnosis with actionable next steps.`,
|
|
89
|
+
inputSchema: {
|
|
90
|
+
runId: z.string().describe("Run ID to debug (e.g. 'run_abc123')"),
|
|
91
|
+
message: z.string().optional().describe("Additional context or specific question about the failure"),
|
|
92
|
+
},
|
|
93
|
+
}, async ({ runId, message }) => {
|
|
94
|
+
try {
|
|
95
|
+
validateRunId(runId);
|
|
96
|
+
auditLog("CADDIE_DEBUG", `run ${runId}`);
|
|
97
|
+
// Fetch run details
|
|
98
|
+
const run = await request(`/v1/runs/${runId}`);
|
|
99
|
+
if (!run)
|
|
100
|
+
throw new Error(`Run ${runId} not found`);
|
|
101
|
+
// Build debug prompt with run context
|
|
102
|
+
const contextParts = [];
|
|
103
|
+
contextParts.push(`Debug this workflow run:`);
|
|
104
|
+
contextParts.push(`- Run ID: ${run.id}`);
|
|
105
|
+
contextParts.push(`- Status: ${run.status}`);
|
|
106
|
+
contextParts.push(`- Workflow ID: ${run.workflowId}`);
|
|
107
|
+
contextParts.push(`- Workflow Version: ${run.workflowVersion}`);
|
|
108
|
+
if (run.startedAt)
|
|
109
|
+
contextParts.push(`- Started: ${run.startedAt}`);
|
|
110
|
+
if (run.finishedAt)
|
|
111
|
+
contextParts.push(`- Finished: ${run.finishedAt}`);
|
|
112
|
+
if (run.triggerSource)
|
|
113
|
+
contextParts.push(`- Trigger: ${run.triggerSource}`);
|
|
114
|
+
if (run.executionState) {
|
|
115
|
+
// Truncate to avoid blowing Caddie's token budget on large execution states
|
|
116
|
+
const stateJson = JSON.stringify(run.executionState, null, 2);
|
|
117
|
+
contextParts.push(`\nExecution state:\n${stateJson.length > 30_000 ? stateJson.slice(0, 30_000) + "\n...(truncated)" : stateJson}`);
|
|
118
|
+
}
|
|
119
|
+
if (message) {
|
|
120
|
+
contextParts.push(`\nUser context: ${message}`);
|
|
121
|
+
}
|
|
122
|
+
const debugPrompt = contextParts.join("\n");
|
|
123
|
+
const apiKey = getApiKey();
|
|
124
|
+
const serverUrl = getServerUrl();
|
|
125
|
+
const done = await consumeCaddieStream(apiKey, serverUrl, {
|
|
126
|
+
message: debugPrompt,
|
|
127
|
+
workflowId: run.workflowId,
|
|
128
|
+
});
|
|
129
|
+
if (done.data.type === "workflow" && done.data.workflow) {
|
|
130
|
+
const text = formatWorkflowResponse(done, `Caddie diagnosed run ${runId} and produced a fixed definition:\n`, `1. Review the fixed definition\n2. Use b3os_validate_workflow to verify\n3. Use b3os_update_workflow to save it to workflow ${run.workflowId}`);
|
|
131
|
+
return { content: [{ type: "text", text }] };
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
content: [
|
|
135
|
+
{
|
|
136
|
+
type: "text",
|
|
137
|
+
text: formatCaddieBlocksForCLI(done.data.message || `Caddie analyzed run ${runId} but did not return a diagnosis.`, "b3os_debug_run"),
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
return {
|
|
144
|
+
content: [{ type: "text", text: `Caddie debug failed: ${err instanceof Error ? err.message : err}` }],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { request, truncateResponse } from "../client.js";
|
|
3
|
+
import { ACTION_TYPE_RE } from "./shared.js";
|
|
4
|
+
export function registerCatalogTools(s) {
|
|
5
|
+
s.registerTool("b3os_list_actions", {
|
|
6
|
+
description: `Browse the B3OS action catalog. Returns all available actions that can be used as
|
|
7
|
+
workflow nodes. Filter by tags (e.g. "defi", "social", "data").
|
|
8
|
+
|
|
9
|
+
For keyword search, use b3os_search_actions instead — it's faster and more relevant.`,
|
|
10
|
+
inputSchema: {
|
|
11
|
+
tags: z.string().optional().describe("Filter by tags (comma-separated)"),
|
|
12
|
+
limit: z
|
|
13
|
+
.number()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("Max actions to return (default: 50). Use b3os_search_actions for targeted results."),
|
|
16
|
+
},
|
|
17
|
+
}, async ({ tags, limit }) => {
|
|
18
|
+
const params = { summary: "true" };
|
|
19
|
+
if (tags)
|
|
20
|
+
params.tags = tags;
|
|
21
|
+
const actions = await request("/v1/actions", { params, noAuth: true });
|
|
22
|
+
const allActions = actions || [];
|
|
23
|
+
const safeLimit = limit ?? 50;
|
|
24
|
+
const truncated = allActions.length > safeLimit;
|
|
25
|
+
const items = (truncated ? allActions.slice(0, safeLimit) : allActions).map(a => ({
|
|
26
|
+
type: a.type,
|
|
27
|
+
name: a.name,
|
|
28
|
+
description: a.description,
|
|
29
|
+
category: a.category,
|
|
30
|
+
tags: a.tags,
|
|
31
|
+
requiresGas: a.requiresGas,
|
|
32
|
+
connector: a.connector?.type,
|
|
33
|
+
}));
|
|
34
|
+
const note = truncated
|
|
35
|
+
? `\n\nShowing ${safeLimit} of ${allActions.length}. Use b3os_search_actions for targeted results.`
|
|
36
|
+
: "";
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: "text",
|
|
41
|
+
text: JSON.stringify(items, null, 2) + note,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
s.registerTool("b3os_search_actions", {
|
|
47
|
+
description: `Search for B3OS actions by keyword. Uses hybrid search (keyword + semantic) to
|
|
48
|
+
find the most relevant actions for your workflow. Returns top matches with name,
|
|
49
|
+
description, category, and tags.
|
|
50
|
+
|
|
51
|
+
Use this FIRST when building a workflow to find the right action types. Then use
|
|
52
|
+
b3os_get_action to get the full payload/result schemas for the actions you choose.
|
|
53
|
+
|
|
54
|
+
Examples: "send slack message", "swap tokens", "fetch token price", "google sheets"`,
|
|
55
|
+
inputSchema: {
|
|
56
|
+
query: z.string().min(1).describe("Search query (e.g. 'send slack message', 'token price')"),
|
|
57
|
+
limit: z.number().optional().describe("Max results (default: 10)"),
|
|
58
|
+
},
|
|
59
|
+
}, async ({ query, limit }) => {
|
|
60
|
+
const safeLimit = Math.min(limit || 10, 50);
|
|
61
|
+
const params = {
|
|
62
|
+
q: query,
|
|
63
|
+
types: "action",
|
|
64
|
+
limit: String(safeLimit),
|
|
65
|
+
};
|
|
66
|
+
try {
|
|
67
|
+
const results = await request("/v1/search", { params });
|
|
68
|
+
const hits = results?.results || [];
|
|
69
|
+
return {
|
|
70
|
+
content: [
|
|
71
|
+
{
|
|
72
|
+
type: "text",
|
|
73
|
+
text: hits.length > 0
|
|
74
|
+
? JSON.stringify(hits.map(h => ({
|
|
75
|
+
type: h.id,
|
|
76
|
+
name: h.name,
|
|
77
|
+
description: h.description,
|
|
78
|
+
category: h.category,
|
|
79
|
+
tags: h.tags,
|
|
80
|
+
})), null, 2)
|
|
81
|
+
: `No actions found for "${query}". Try broader terms or use b3os_list_actions to browse all.`,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: "text",
|
|
91
|
+
text: `Search unavailable — use b3os_list_actions to browse the full catalog instead.`,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
s.registerTool("b3os_get_action", {
|
|
98
|
+
description: `Get full details of a specific action, including its payload schema (input fields)
|
|
99
|
+
and result schema (output fields). Use the 'type' field from search results
|
|
100
|
+
(e.g. "coingecko-get-token-price", "send-erc20-token", "slack-send-message").
|
|
101
|
+
|
|
102
|
+
Read the payload schema to understand what fields to set in the node's payload,
|
|
103
|
+
and the result schema to understand what data flows to downstream nodes.`,
|
|
104
|
+
inputSchema: { type: z.string().describe("Action type identifier (e.g. 'coingecko-get-token-price')") },
|
|
105
|
+
}, async ({ type }) => {
|
|
106
|
+
if (!ACTION_TYPE_RE.test(type)) {
|
|
107
|
+
throw new Error(`Invalid action type format: "${type}". Use b3os_search_actions to find valid types.`);
|
|
108
|
+
}
|
|
109
|
+
const action = await request(`/v1/actions/${type}`, { noAuth: true });
|
|
110
|
+
if (!action)
|
|
111
|
+
throw new Error(`Action "${type}" not found`);
|
|
112
|
+
return { content: [{ type: "text", text: truncateResponse(JSON.stringify(action, null, 2)) }] };
|
|
113
|
+
});
|
|
114
|
+
s.registerTool("b3os_list_triggers", {
|
|
115
|
+
description: `Browse available trigger types. Triggers start workflow execution — schedules (cron),
|
|
116
|
+
webhooks, blockchain events, manual triggers, etc. Filter by tags.`,
|
|
117
|
+
inputSchema: { tags: z.string().optional().describe("Filter by tags (comma-separated)") },
|
|
118
|
+
}, async ({ tags }) => {
|
|
119
|
+
const params = { summary: "true" };
|
|
120
|
+
if (tags)
|
|
121
|
+
params.tags = tags;
|
|
122
|
+
const triggers = await request("/v1/triggers", { params, noAuth: true });
|
|
123
|
+
return {
|
|
124
|
+
content: [
|
|
125
|
+
{
|
|
126
|
+
type: "text",
|
|
127
|
+
text: JSON.stringify((triggers || []).map(t => ({
|
|
128
|
+
type: t.type,
|
|
129
|
+
name: t.name,
|
|
130
|
+
description: t.description,
|
|
131
|
+
tags: t.tags,
|
|
132
|
+
connector: t.connector?.type,
|
|
133
|
+
})), null, 2),
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
s.registerTool("b3os_get_trigger", {
|
|
139
|
+
description: `Get full details of a specific trigger type, including its payload schema.
|
|
140
|
+
Use the 'type' field from b3os_list_triggers (e.g. "cronjob", "webhook").`,
|
|
141
|
+
inputSchema: { type: z.string().describe("Trigger type identifier (e.g. 'cronjob')") },
|
|
142
|
+
}, async ({ type }) => {
|
|
143
|
+
if (!ACTION_TYPE_RE.test(type)) {
|
|
144
|
+
throw new Error(`Invalid trigger type format: "${type}". Use b3os_list_triggers to browse valid types.`);
|
|
145
|
+
}
|
|
146
|
+
const trigger = await request(`/v1/triggers/${type}`, { noAuth: true });
|
|
147
|
+
if (!trigger)
|
|
148
|
+
throw new Error(`Trigger "${type}" not found`);
|
|
149
|
+
return { content: [{ type: "text", text: truncateResponse(JSON.stringify(trigger, null, 2)) }] };
|
|
150
|
+
});
|
|
151
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ApiError, request, truncateResponse } from "../client.js";
|
|
3
|
+
import { validateOrgId } from "./shared.js";
|
|
4
|
+
export function registerConnectorTools(s) {
|
|
5
|
+
s.registerTool("b3os_list_wallets", {
|
|
6
|
+
description: `List wallets for the organization. Wallets are blockchain addresses managed by B3OS
|
|
7
|
+
(HSM-backed). Useful for workflows that send tokens or interact with DeFi protocols.
|
|
8
|
+
|
|
9
|
+
Requires orgId — call b3os_whoami first to get it.`,
|
|
10
|
+
inputSchema: { orgId: z.string().describe("Organization ID from b3os_whoami") },
|
|
11
|
+
}, async ({ orgId }) => {
|
|
12
|
+
validateOrgId(orgId);
|
|
13
|
+
const allWallets = [];
|
|
14
|
+
const limit = 50;
|
|
15
|
+
const maxPages = 20;
|
|
16
|
+
const deadline = Date.now() + 30_000;
|
|
17
|
+
let offset = 0;
|
|
18
|
+
for (let page = 0; page < maxPages; page++) {
|
|
19
|
+
const remaining = deadline - Date.now();
|
|
20
|
+
if (remaining <= 0)
|
|
21
|
+
break;
|
|
22
|
+
const data = await request(`/v1/organizations/${orgId}/wallets`, {
|
|
23
|
+
params: { limit: String(limit), offset: String(offset) },
|
|
24
|
+
timeout: Math.max(remaining, 1000),
|
|
25
|
+
});
|
|
26
|
+
const items = data?.items || [];
|
|
27
|
+
allWallets.push(...items);
|
|
28
|
+
if (!data?.hasMore || items.length === 0)
|
|
29
|
+
break;
|
|
30
|
+
offset += items.length;
|
|
31
|
+
}
|
|
32
|
+
const text = JSON.stringify(allWallets.map(w => ({
|
|
33
|
+
id: w.id,
|
|
34
|
+
name: w.name,
|
|
35
|
+
address: w.address,
|
|
36
|
+
isDefault: w.isDefault,
|
|
37
|
+
})), null, 2);
|
|
38
|
+
return { content: [{ type: "text", text: truncateResponse(text) }] };
|
|
39
|
+
});
|
|
40
|
+
s.registerTool("b3os_list_connectors", {
|
|
41
|
+
description: `List configured connectors (OAuth integrations) for the organization. Connectors are
|
|
42
|
+
credentials for external services — Slack, Discord, Google Sheets, wallets, etc.
|
|
43
|
+
|
|
44
|
+
Use this to discover which connectors are available before building workflows. The
|
|
45
|
+
connector ID is needed for workflow nodes that interact with external services:
|
|
46
|
+
"connector": { "type": "slack", "id": "conn_abc123" }
|
|
47
|
+
|
|
48
|
+
Without this tool, you'd have to use placeholder connector references.`,
|
|
49
|
+
}, async () => {
|
|
50
|
+
const data = await request("/v1/connectors", {
|
|
51
|
+
params: { limit: "100" },
|
|
52
|
+
});
|
|
53
|
+
const connectors = data?.items || [];
|
|
54
|
+
if (connectors.length === 0) {
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
type: "text",
|
|
59
|
+
text: "No connectors configured yet.\n\nConnectors link workflows to external services (Slack, Telegram, Gmail, wallets, etc.). Set up your first connector at: https://b3os.org/connectors",
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const text = JSON.stringify(connectors.map(c => ({
|
|
65
|
+
id: c.id,
|
|
66
|
+
name: c.name,
|
|
67
|
+
type: c.type,
|
|
68
|
+
provider: c.provider,
|
|
69
|
+
})), null, 2);
|
|
70
|
+
return { content: [{ type: "text", text: truncateResponse(text) }] };
|
|
71
|
+
});
|
|
72
|
+
s.registerTool("b3os_list_telegram_chats", {
|
|
73
|
+
description: `List verified Telegram chats (DMs, groups, channels) for a connector. Use this to
|
|
74
|
+
discover available chatId values before building workflows with send-telegram-message.
|
|
75
|
+
|
|
76
|
+
Returns chatId, chatTitle, chatType (private/group/supergroup/channel), and chatUsername.
|
|
77
|
+
Filter by connectorId to see chats for a specific Telegram bot.
|
|
78
|
+
|
|
79
|
+
Call b3os_list_connectors first to find telegram-bot connector IDs.`,
|
|
80
|
+
inputSchema: {
|
|
81
|
+
connectorId: z.string().optional().describe("Filter by telegram-bot connector ID (from b3os_list_connectors)"),
|
|
82
|
+
query: z.string().optional().describe("Search query to filter chats by title or username"),
|
|
83
|
+
},
|
|
84
|
+
}, async ({ connectorId, query }) => {
|
|
85
|
+
const params = { limit: "50", offset: "0" };
|
|
86
|
+
if (connectorId)
|
|
87
|
+
params.connector_id = connectorId;
|
|
88
|
+
if (query)
|
|
89
|
+
params.query = query;
|
|
90
|
+
const data = await request("/v1/telegram-bot/chats", { params });
|
|
91
|
+
const chats = data?.items || [];
|
|
92
|
+
if (chats.length === 0) {
|
|
93
|
+
const hint = connectorId
|
|
94
|
+
? `No Telegram chats found for connector ${connectorId}. Make sure:\n1. The Telegram bot is running\n2. Users have sent /start to the bot\n3. The bot has been added to any target groups`
|
|
95
|
+
: "No Telegram chats found. Set up a telegram-bot connector at https://b3os.org/connectors first, then send /start to the bot in Telegram.";
|
|
96
|
+
return { content: [{ type: "text", text: hint }] };
|
|
97
|
+
}
|
|
98
|
+
const text = JSON.stringify(chats.map(c => ({
|
|
99
|
+
chatId: c.chatId,
|
|
100
|
+
chatTitle: c.chatTitle,
|
|
101
|
+
chatType: c.chatType,
|
|
102
|
+
chatUsername: c.chatUsername,
|
|
103
|
+
connectorId: c.connectorId,
|
|
104
|
+
connectorName: c.connectorName,
|
|
105
|
+
})), null, 2);
|
|
106
|
+
return { content: [{ type: "text", text: truncateResponse(text) }] };
|
|
107
|
+
});
|
|
108
|
+
s.registerTool("b3os_list_slack_channels", {
|
|
109
|
+
description: `List Slack channels available to a connector. Use this to discover channel IDs
|
|
110
|
+
before building workflows with slack-send-message or slack-post-message.
|
|
111
|
+
|
|
112
|
+
Returns channel id, name, type (public/private), and member count.
|
|
113
|
+
Requires a slack connector ID — call b3os_list_connectors first.`,
|
|
114
|
+
inputSchema: {
|
|
115
|
+
connectorId: z.string().describe("Slack connector ID (from b3os_list_connectors)"),
|
|
116
|
+
},
|
|
117
|
+
}, async ({ connectorId }) => {
|
|
118
|
+
const seen = new Set();
|
|
119
|
+
const allChannels = [];
|
|
120
|
+
const maxPages = 3;
|
|
121
|
+
let cursor;
|
|
122
|
+
for (let page = 0; page < maxPages; page++) {
|
|
123
|
+
let data;
|
|
124
|
+
try {
|
|
125
|
+
const inputs = { limit: 1000 };
|
|
126
|
+
if (cursor)
|
|
127
|
+
inputs.cursor = cursor;
|
|
128
|
+
data = await request("/v1/actions/slack-list-channels/test", {
|
|
129
|
+
method: "POST",
|
|
130
|
+
body: {
|
|
131
|
+
inputs,
|
|
132
|
+
connector: { id: connectorId, type: "slack" },
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
if (err instanceof ApiError && err.status === 400) {
|
|
138
|
+
return {
|
|
139
|
+
content: [
|
|
140
|
+
{
|
|
141
|
+
type: "text",
|
|
142
|
+
text: `Slack connector "${connectorId}" not found. Run b3os_list_connectors to see available connectors, or set up a new Slack connector at: https://b3os.org/connectors`,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// Surface transient errors instead of silently returning empty results
|
|
148
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text", text: `Failed to fetch Slack channels: ${msg}` }],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const ret = data?.result?.data?.ret;
|
|
154
|
+
if (!ret || typeof ret !== "object")
|
|
155
|
+
break;
|
|
156
|
+
const channels = ret.channels;
|
|
157
|
+
let newCount = 0;
|
|
158
|
+
if (Array.isArray(channels)) {
|
|
159
|
+
for (const ch of channels) {
|
|
160
|
+
if (ch &&
|
|
161
|
+
typeof ch.id === "string" &&
|
|
162
|
+
typeof ch.name === "string" &&
|
|
163
|
+
!ch.is_archived &&
|
|
164
|
+
ch.num_members > 0 &&
|
|
165
|
+
!seen.has(ch.id)) {
|
|
166
|
+
seen.add(ch.id);
|
|
167
|
+
allChannels.push({
|
|
168
|
+
id: ch.id,
|
|
169
|
+
name: ch.name,
|
|
170
|
+
is_private: ch.is_private || ch.is_group,
|
|
171
|
+
num_members: ch.num_members,
|
|
172
|
+
});
|
|
173
|
+
newCount++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const nextCursor = ret.next_cursor;
|
|
178
|
+
if (!nextCursor || typeof nextCursor !== "string" || newCount === 0)
|
|
179
|
+
break;
|
|
180
|
+
cursor = nextCursor;
|
|
181
|
+
}
|
|
182
|
+
if (allChannels.length === 0) {
|
|
183
|
+
return {
|
|
184
|
+
content: [
|
|
185
|
+
{
|
|
186
|
+
type: "text",
|
|
187
|
+
text: `No Slack channels found for connector ${connectorId}. Make sure the B3OS Slack app has been added to channels in your workspace.`,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const text = JSON.stringify(allChannels.map(c => ({
|
|
193
|
+
id: c.id,
|
|
194
|
+
name: c.name,
|
|
195
|
+
type: c.is_private ? "private" : "public",
|
|
196
|
+
members: c.num_members,
|
|
197
|
+
})), null, 2);
|
|
198
|
+
return { content: [{ type: "text", text: truncateResponse(text) }] };
|
|
199
|
+
});
|
|
200
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { request, truncateResponse } from "../client.js";
|
|
3
|
+
import { validateOrgId } from "./shared.js";
|
|
4
|
+
export function registerDatabaseTools(s) {
|
|
5
|
+
s.registerTool("b3os_list_tables", {
|
|
6
|
+
description: `List all tables in the organization's database.
|
|
7
|
+
|
|
8
|
+
Each org gets one auto-provisioned database. Use this to discover existing tables
|
|
9
|
+
before creating new ones or writing queries. Requires orgId from b3os_whoami.`,
|
|
10
|
+
inputSchema: { orgId: z.string().describe("Organization ID from b3os_whoami") },
|
|
11
|
+
}, async ({ orgId }) => {
|
|
12
|
+
validateOrgId(orgId);
|
|
13
|
+
const data = await request(`/v1/organizations/${orgId}/database/tables`);
|
|
14
|
+
const tables = data?.tables || [];
|
|
15
|
+
if (tables.length === 0) {
|
|
16
|
+
return { content: [{ type: "text", text: "No tables found. The database is empty." }] };
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
content: [{ type: "text", text: tables.map(t => t.name).join("\n") }],
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
s.registerTool("b3os_get_table_schema", {
|
|
23
|
+
description: `Get the column definitions for a specific table in the org's database.
|
|
24
|
+
|
|
25
|
+
Returns each column's name, type, constraints (NOT NULL, PRIMARY KEY), and default value.
|
|
26
|
+
Use this to understand table structure before writing queries.`,
|
|
27
|
+
inputSchema: {
|
|
28
|
+
orgId: z.string().describe("Organization ID from b3os_whoami"),
|
|
29
|
+
tableName: z.string().describe("Table name to inspect"),
|
|
30
|
+
},
|
|
31
|
+
}, async ({ orgId, tableName }) => {
|
|
32
|
+
validateOrgId(orgId);
|
|
33
|
+
const data = await request(`/v1/organizations/${orgId}/database/tables/${encodeURIComponent(tableName)}`);
|
|
34
|
+
const columns = data?.columns || [];
|
|
35
|
+
const text = JSON.stringify(columns, null, 2);
|
|
36
|
+
return { content: [{ type: "text", text }] };
|
|
37
|
+
});
|
|
38
|
+
s.registerTool("b3os_query_database", {
|
|
39
|
+
description: `Execute SQL against the organization's database (SQLite-compatible).
|
|
40
|
+
|
|
41
|
+
Supported statements: SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, ALTER TABLE, DROP TABLE.
|
|
42
|
+
Use parameterized queries with ? placeholders to prevent SQL injection.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
- SELECT: { rows: [...], rowsAffected: 0 }
|
|
46
|
+
- INSERT/UPDATE/DELETE: { rows: [], rowsAffected: N, lastRowId: N }
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
- SELECT * FROM budgets WHERE workflow_id = ? (params: ["my-workflow"])
|
|
50
|
+
- CREATE TABLE budgets (workflow_id TEXT PRIMARY KEY, spent REAL DEFAULT 0)
|
|
51
|
+
- INSERT INTO budgets (workflow_id, spent) VALUES (?, ?) (params: ["wf1", 10.5])`,
|
|
52
|
+
inputSchema: {
|
|
53
|
+
orgId: z.string().describe("Organization ID from b3os_whoami"),
|
|
54
|
+
sql: z.string().describe("SQL statement to execute"),
|
|
55
|
+
params: z.array(z.any()).optional().describe("Parameterized query values (for ? placeholders)"),
|
|
56
|
+
},
|
|
57
|
+
}, async ({ orgId, sql, params }) => {
|
|
58
|
+
validateOrgId(orgId);
|
|
59
|
+
const data = await request(`/v1/organizations/${orgId}/database/query`, {
|
|
60
|
+
method: "POST",
|
|
61
|
+
body: { sql, params: params || [] },
|
|
62
|
+
});
|
|
63
|
+
const text = JSON.stringify(data, null, 2);
|
|
64
|
+
return { content: [{ type: "text", text: truncateResponse(text) }] };
|
|
65
|
+
});
|
|
66
|
+
}
|