@secondlayer/mcp 0.4.2 → 1.0.1
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 +3 -6
- package/dist/bin-http.js +351 -658
- package/dist/bin-http.js.map +11 -15
- package/dist/bin.js +351 -658
- package/dist/bin.js.map +11 -15
- package/dist/index.js +351 -658
- package/dist/index.js.map +11 -15
- package/package.json +8 -5
package/dist/bin-http.js
CHANGED
|
@@ -13,10 +13,80 @@ import { fileURLToPath } from "node:url";
|
|
|
13
13
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
14
|
|
|
15
15
|
// src/resources.ts
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
var FILTERS_REFERENCE = [
|
|
17
|
+
{
|
|
18
|
+
type: "stx_transfer",
|
|
19
|
+
fields: ["sender", "recipient", "minAmount", "maxAmount"]
|
|
20
|
+
},
|
|
21
|
+
{ type: "stx_mint", fields: ["recipient", "minAmount"] },
|
|
22
|
+
{ type: "stx_burn", fields: ["sender", "minAmount"] },
|
|
23
|
+
{ type: "stx_lock", fields: ["lockedAddress", "minAmount"] },
|
|
24
|
+
{
|
|
25
|
+
type: "ft_transfer",
|
|
26
|
+
fields: [
|
|
27
|
+
"sender",
|
|
28
|
+
"recipient",
|
|
29
|
+
"assetIdentifier",
|
|
30
|
+
"minAmount",
|
|
31
|
+
"maxAmount"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{ type: "ft_mint", fields: ["recipient", "assetIdentifier", "minAmount"] },
|
|
35
|
+
{ type: "ft_burn", fields: ["sender", "assetIdentifier", "minAmount"] },
|
|
36
|
+
{
|
|
37
|
+
type: "nft_transfer",
|
|
38
|
+
fields: ["sender", "recipient", "assetIdentifier", "tokenId"]
|
|
39
|
+
},
|
|
40
|
+
{ type: "nft_mint", fields: ["recipient", "assetIdentifier", "tokenId"] },
|
|
41
|
+
{ type: "nft_burn", fields: ["sender", "assetIdentifier", "tokenId"] },
|
|
42
|
+
{ type: "contract_call", fields: ["contract", "function"] },
|
|
43
|
+
{ type: "contract_deploy", fields: ["contract"] },
|
|
44
|
+
{ type: "print_event", fields: ["contract", "event", "contains"] }
|
|
45
|
+
];
|
|
46
|
+
var COLUMN_TYPES = [
|
|
47
|
+
{
|
|
48
|
+
type: "uint",
|
|
49
|
+
sqlType: "bigint",
|
|
50
|
+
description: "Unsigned integer (Clarity uint)"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: "int",
|
|
54
|
+
sqlType: "bigint",
|
|
55
|
+
description: "Signed integer (Clarity int)"
|
|
56
|
+
},
|
|
57
|
+
{ type: "text", sqlType: "text", description: "UTF-8 string" },
|
|
58
|
+
{
|
|
59
|
+
type: "principal",
|
|
60
|
+
sqlType: "text",
|
|
61
|
+
description: "Stacks address (standard or contract)"
|
|
62
|
+
},
|
|
63
|
+
{ type: "bool", sqlType: "boolean", description: "Boolean value" },
|
|
64
|
+
{ type: "json", sqlType: "jsonb", description: "Arbitrary JSON data" },
|
|
65
|
+
{
|
|
66
|
+
options: ["nullable", "indexed", "search"],
|
|
67
|
+
description: "Column options: nullable allows NULL, indexed creates a B-tree index, search enables full-text search"
|
|
68
|
+
}
|
|
69
|
+
];
|
|
70
|
+
function registerResources(server) {
|
|
71
|
+
server.resource("filters", "secondlayer://filters", { description: "Event filter types and their available fields" }, async () => ({
|
|
72
|
+
contents: [
|
|
73
|
+
{
|
|
74
|
+
uri: "secondlayer://filters",
|
|
75
|
+
mimeType: "application/json",
|
|
76
|
+
text: JSON.stringify(FILTERS_REFERENCE, null, 2)
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}));
|
|
80
|
+
server.resource("column-types", "secondlayer://column-types", { description: "Subgraph column types, SQL mappings, and options" }, async () => ({
|
|
81
|
+
contents: [
|
|
82
|
+
{
|
|
83
|
+
uri: "secondlayer://column-types",
|
|
84
|
+
mimeType: "application/json",
|
|
85
|
+
text: JSON.stringify(COLUMN_TYPES, null, 2)
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
20
90
|
|
|
21
91
|
// src/lib/client.ts
|
|
22
92
|
import { SecondLayer } from "@secondlayer/sdk";
|
|
@@ -27,7 +97,12 @@ function getClient() {
|
|
|
27
97
|
if (!apiKey) {
|
|
28
98
|
throw new Error("SECONDLAYER_API_KEY environment variable is required. " + "Get your key at https://app.secondlayer.tools/settings/api-keys");
|
|
29
99
|
}
|
|
30
|
-
|
|
100
|
+
const baseUrl = process.env.SECONDLAYER_API_URL;
|
|
101
|
+
instance = new SecondLayer({
|
|
102
|
+
apiKey,
|
|
103
|
+
origin: "mcp",
|
|
104
|
+
...baseUrl ? { baseUrl } : {}
|
|
105
|
+
});
|
|
31
106
|
}
|
|
32
107
|
return instance;
|
|
33
108
|
}
|
|
@@ -56,16 +131,6 @@ async function apiRequest(method, path, body) {
|
|
|
56
131
|
}
|
|
57
132
|
|
|
58
133
|
// src/lib/format.ts
|
|
59
|
-
function formatStreamSummary(s) {
|
|
60
|
-
return {
|
|
61
|
-
id: s.id,
|
|
62
|
-
name: s.name,
|
|
63
|
-
status: s.status,
|
|
64
|
-
endpointUrl: s.endpointUrl,
|
|
65
|
-
totalDeliveries: s.totalDeliveries,
|
|
66
|
-
failedDeliveries: s.failedDeliveries
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
134
|
function formatSubgraphSummary(s) {
|
|
70
135
|
return {
|
|
71
136
|
name: s.name,
|
|
@@ -74,16 +139,6 @@ function formatSubgraphSummary(s) {
|
|
|
74
139
|
lastProcessedBlock: s.lastProcessedBlock
|
|
75
140
|
};
|
|
76
141
|
}
|
|
77
|
-
function formatDeliverySummary(d) {
|
|
78
|
-
return {
|
|
79
|
-
id: d.id,
|
|
80
|
-
blockHeight: d.blockHeight,
|
|
81
|
-
status: d.status,
|
|
82
|
-
statusCode: d.statusCode,
|
|
83
|
-
attempts: d.attempts,
|
|
84
|
-
createdAt: d.createdAt
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
142
|
function withCap(items, cap) {
|
|
88
143
|
return {
|
|
89
144
|
items: items.slice(0, cap),
|
|
@@ -91,6 +146,12 @@ function withCap(items, cap) {
|
|
|
91
146
|
total: items.length
|
|
92
147
|
};
|
|
93
148
|
}
|
|
149
|
+
function jsonResponse(data, isError) {
|
|
150
|
+
return {
|
|
151
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
152
|
+
...isError && { isError: true }
|
|
153
|
+
};
|
|
154
|
+
}
|
|
94
155
|
|
|
95
156
|
// src/lib/tool.ts
|
|
96
157
|
function defineTool(server, name, description, schema, handler) {
|
|
@@ -115,514 +176,17 @@ function defineTool(server, name, description, schema, handler) {
|
|
|
115
176
|
server.tool(name, description, schema, wrappedHandler);
|
|
116
177
|
}
|
|
117
178
|
|
|
118
|
-
// src/tools/streams.ts
|
|
119
|
-
var FilterSchema = z.discriminatedUnion("type", [
|
|
120
|
-
z.object({
|
|
121
|
-
type: z.literal("stx_transfer"),
|
|
122
|
-
sender: z.string().optional(),
|
|
123
|
-
recipient: z.string().optional(),
|
|
124
|
-
minAmount: z.number().optional(),
|
|
125
|
-
maxAmount: z.number().optional()
|
|
126
|
-
}),
|
|
127
|
-
z.object({
|
|
128
|
-
type: z.literal("stx_mint"),
|
|
129
|
-
recipient: z.string().optional(),
|
|
130
|
-
minAmount: z.number().optional()
|
|
131
|
-
}),
|
|
132
|
-
z.object({
|
|
133
|
-
type: z.literal("stx_burn"),
|
|
134
|
-
sender: z.string().optional(),
|
|
135
|
-
minAmount: z.number().optional()
|
|
136
|
-
}),
|
|
137
|
-
z.object({
|
|
138
|
-
type: z.literal("stx_lock"),
|
|
139
|
-
lockedAddress: z.string().optional(),
|
|
140
|
-
minAmount: z.number().optional()
|
|
141
|
-
}),
|
|
142
|
-
z.object({
|
|
143
|
-
type: z.literal("ft_transfer"),
|
|
144
|
-
sender: z.string().optional(),
|
|
145
|
-
recipient: z.string().optional(),
|
|
146
|
-
assetIdentifier: z.string().optional(),
|
|
147
|
-
minAmount: z.number().optional()
|
|
148
|
-
}),
|
|
149
|
-
z.object({
|
|
150
|
-
type: z.literal("ft_mint"),
|
|
151
|
-
recipient: z.string().optional(),
|
|
152
|
-
assetIdentifier: z.string().optional(),
|
|
153
|
-
minAmount: z.number().optional()
|
|
154
|
-
}),
|
|
155
|
-
z.object({
|
|
156
|
-
type: z.literal("ft_burn"),
|
|
157
|
-
sender: z.string().optional(),
|
|
158
|
-
assetIdentifier: z.string().optional(),
|
|
159
|
-
minAmount: z.number().optional()
|
|
160
|
-
}),
|
|
161
|
-
z.object({
|
|
162
|
-
type: z.literal("nft_transfer"),
|
|
163
|
-
sender: z.string().optional(),
|
|
164
|
-
recipient: z.string().optional(),
|
|
165
|
-
assetIdentifier: z.string().optional(),
|
|
166
|
-
tokenId: z.string().optional()
|
|
167
|
-
}),
|
|
168
|
-
z.object({
|
|
169
|
-
type: z.literal("nft_mint"),
|
|
170
|
-
recipient: z.string().optional(),
|
|
171
|
-
assetIdentifier: z.string().optional(),
|
|
172
|
-
tokenId: z.string().optional()
|
|
173
|
-
}),
|
|
174
|
-
z.object({
|
|
175
|
-
type: z.literal("nft_burn"),
|
|
176
|
-
sender: z.string().optional(),
|
|
177
|
-
assetIdentifier: z.string().optional(),
|
|
178
|
-
tokenId: z.string().optional()
|
|
179
|
-
}),
|
|
180
|
-
z.object({
|
|
181
|
-
type: z.literal("contract_call"),
|
|
182
|
-
contractId: z.string().optional(),
|
|
183
|
-
functionName: z.string().optional(),
|
|
184
|
-
caller: z.string().optional()
|
|
185
|
-
}),
|
|
186
|
-
z.object({
|
|
187
|
-
type: z.literal("contract_deploy"),
|
|
188
|
-
deployer: z.string().optional(),
|
|
189
|
-
contractName: z.string().optional()
|
|
190
|
-
}),
|
|
191
|
-
z.object({
|
|
192
|
-
type: z.literal("print_event"),
|
|
193
|
-
contractId: z.string().optional(),
|
|
194
|
-
topic: z.string().optional(),
|
|
195
|
-
contains: z.string().optional()
|
|
196
|
-
})
|
|
197
|
-
]);
|
|
198
|
-
function registerStreamTools(server) {
|
|
199
|
-
defineTool(server, "streams_list", "List all webhook streams. Returns summary fields only.", {
|
|
200
|
-
status: z.enum(["active", "inactive", "paused", "failed"]).optional().describe("Filter by status")
|
|
201
|
-
}, async ({ status }) => {
|
|
202
|
-
const { streams } = await getClient().streams.list(status ? { status } : undefined);
|
|
203
|
-
return {
|
|
204
|
-
content: [
|
|
205
|
-
{
|
|
206
|
-
type: "text",
|
|
207
|
-
text: JSON.stringify(streams.map(formatStreamSummary), null, 2)
|
|
208
|
-
}
|
|
209
|
-
]
|
|
210
|
-
};
|
|
211
|
-
});
|
|
212
|
-
defineTool(server, "streams_get", "Get full details of a stream by ID (accepts UUID prefix).", { id: z.string().describe("Stream UUID or prefix") }, async ({ id }) => {
|
|
213
|
-
const stream = await getClient().streams.get(id);
|
|
214
|
-
return {
|
|
215
|
-
content: [{ type: "text", text: JSON.stringify(stream, null, 2) }]
|
|
216
|
-
};
|
|
217
|
-
});
|
|
218
|
-
defineTool(server, "streams_create", "Create a new webhook stream with filters.", {
|
|
219
|
-
name: z.string().describe("Stream name"),
|
|
220
|
-
endpointUrl: z.string().describe("Webhook endpoint URL"),
|
|
221
|
-
filters: z.array(FilterSchema).min(1).describe("Event filters (at least one required)")
|
|
222
|
-
}, async ({ name, endpointUrl, filters }) => {
|
|
223
|
-
const result = await getClient().streams.create({
|
|
224
|
-
name,
|
|
225
|
-
endpointUrl,
|
|
226
|
-
filters
|
|
227
|
-
});
|
|
228
|
-
return {
|
|
229
|
-
content: [
|
|
230
|
-
{
|
|
231
|
-
type: "text",
|
|
232
|
-
text: JSON.stringify({ id: result.stream.id, signingSecret: result.signingSecret }, null, 2)
|
|
233
|
-
}
|
|
234
|
-
]
|
|
235
|
-
};
|
|
236
|
-
});
|
|
237
|
-
defineTool(server, "streams_update", "Update a stream's name, endpoint, or filters.", {
|
|
238
|
-
id: z.string().describe("Stream UUID or prefix"),
|
|
239
|
-
name: z.string().optional().describe("New name"),
|
|
240
|
-
endpointUrl: z.string().optional().describe("New endpoint URL"),
|
|
241
|
-
filters: z.array(FilterSchema).min(1).optional().describe("New filters")
|
|
242
|
-
}, async ({ id, name, endpointUrl, filters }) => {
|
|
243
|
-
const data = {};
|
|
244
|
-
if (name !== undefined)
|
|
245
|
-
data.name = name;
|
|
246
|
-
if (endpointUrl !== undefined)
|
|
247
|
-
data.endpointUrl = endpointUrl;
|
|
248
|
-
if (filters !== undefined)
|
|
249
|
-
data.filters = filters;
|
|
250
|
-
const stream = await getClient().streams.update(id, data);
|
|
251
|
-
return {
|
|
252
|
-
content: [
|
|
253
|
-
{
|
|
254
|
-
type: "text",
|
|
255
|
-
text: JSON.stringify(formatStreamSummary(stream), null, 2)
|
|
256
|
-
}
|
|
257
|
-
]
|
|
258
|
-
};
|
|
259
|
-
});
|
|
260
|
-
defineTool(server, "streams_delete", "Delete a stream permanently.", { id: z.string().describe("Stream UUID or prefix") }, async ({ id }) => {
|
|
261
|
-
await getClient().streams.delete(id);
|
|
262
|
-
return { content: [{ type: "text", text: `Stream ${id} deleted.` }] };
|
|
263
|
-
});
|
|
264
|
-
defineTool(server, "streams_toggle", "Enable or disable a stream.", {
|
|
265
|
-
id: z.string().describe("Stream UUID or prefix"),
|
|
266
|
-
enabled: z.boolean().describe("true to enable, false to disable")
|
|
267
|
-
}, async ({ id, enabled }) => {
|
|
268
|
-
const stream = enabled ? await getClient().streams.enable(id) : await getClient().streams.disable(id);
|
|
269
|
-
return {
|
|
270
|
-
content: [
|
|
271
|
-
{
|
|
272
|
-
type: "text",
|
|
273
|
-
text: JSON.stringify({ id: stream.id, status: stream.status }, null, 2)
|
|
274
|
-
}
|
|
275
|
-
]
|
|
276
|
-
};
|
|
277
|
-
});
|
|
278
|
-
defineTool(server, "streams_deliveries", "List recent deliveries for a stream (max 25).", {
|
|
279
|
-
id: z.string().describe("Stream UUID or prefix"),
|
|
280
|
-
limit: z.number().max(25).optional().describe("Max results (default 25)"),
|
|
281
|
-
status: z.string().optional().describe("Filter by delivery status")
|
|
282
|
-
}, async ({ id, limit, status }) => {
|
|
283
|
-
const { deliveries } = await getClient().streams.listDeliveries(id, {
|
|
284
|
-
limit: limit ?? 25,
|
|
285
|
-
status
|
|
286
|
-
});
|
|
287
|
-
const result = withCap(deliveries.map(formatDeliverySummary), 25);
|
|
288
|
-
return {
|
|
289
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
290
|
-
};
|
|
291
|
-
});
|
|
292
|
-
defineTool(server, "streams_pause_all", "Pause all active streams. Without confirm: true, returns a preview of streams that would be paused.", {
|
|
293
|
-
confirm: z.boolean().optional().describe("Set to true to execute. Omit or false for preview only.")
|
|
294
|
-
}, async ({ confirm }) => {
|
|
295
|
-
if (!confirm) {
|
|
296
|
-
const { streams } = await getClient().streams.list({
|
|
297
|
-
status: "active"
|
|
298
|
-
});
|
|
299
|
-
const names = streams.map((s) => s.name);
|
|
300
|
-
return {
|
|
301
|
-
content: [
|
|
302
|
-
{
|
|
303
|
-
type: "text",
|
|
304
|
-
text: JSON.stringify({ preview: true, count: names.length, streams: names }, null, 2)
|
|
305
|
-
}
|
|
306
|
-
]
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
const result = await getClient().streams.pauseAll();
|
|
310
|
-
return {
|
|
311
|
-
content: [
|
|
312
|
-
{
|
|
313
|
-
type: "text",
|
|
314
|
-
text: JSON.stringify({
|
|
315
|
-
paused: result.paused,
|
|
316
|
-
streams: result.streams.map(formatStreamSummary)
|
|
317
|
-
}, null, 2)
|
|
318
|
-
}
|
|
319
|
-
]
|
|
320
|
-
};
|
|
321
|
-
});
|
|
322
|
-
defineTool(server, "streams_resume_all", "Resume all paused streams. Without confirm: true, returns a preview of streams that would be resumed.", {
|
|
323
|
-
confirm: z.boolean().optional().describe("Set to true to execute. Omit or false for preview only.")
|
|
324
|
-
}, async ({ confirm }) => {
|
|
325
|
-
if (!confirm) {
|
|
326
|
-
const { streams } = await getClient().streams.list({
|
|
327
|
-
status: "paused"
|
|
328
|
-
});
|
|
329
|
-
const names = streams.map((s) => s.name);
|
|
330
|
-
return {
|
|
331
|
-
content: [
|
|
332
|
-
{
|
|
333
|
-
type: "text",
|
|
334
|
-
text: JSON.stringify({ preview: true, count: names.length, streams: names }, null, 2)
|
|
335
|
-
}
|
|
336
|
-
]
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
const result = await getClient().streams.resumeAll();
|
|
340
|
-
return {
|
|
341
|
-
content: [
|
|
342
|
-
{
|
|
343
|
-
type: "text",
|
|
344
|
-
text: JSON.stringify({
|
|
345
|
-
resumed: result.resumed,
|
|
346
|
-
streams: result.streams.map(formatStreamSummary)
|
|
347
|
-
}, null, 2)
|
|
348
|
-
}
|
|
349
|
-
]
|
|
350
|
-
};
|
|
351
|
-
});
|
|
352
|
-
defineTool(server, "streams_replay", "Replay blocks through a stream, re-delivering events for a block range.", {
|
|
353
|
-
id: z.string().describe("Stream UUID or prefix"),
|
|
354
|
-
fromBlock: z.number().describe("Start block height (inclusive)"),
|
|
355
|
-
toBlock: z.number().describe("End block height (inclusive)")
|
|
356
|
-
}, async ({ id, fromBlock, toBlock }) => {
|
|
357
|
-
const result = await apiRequest("POST", `/api/streams/${id}/replay`, { fromBlock, toBlock });
|
|
358
|
-
return {
|
|
359
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
360
|
-
};
|
|
361
|
-
});
|
|
362
|
-
defineTool(server, "streams_rotate_secret", "Rotate the signing secret for a stream. Returns the new secret.", { id: z.string().describe("Stream UUID or prefix") }, async ({ id }) => {
|
|
363
|
-
const result = await getClient().streams.rotateSecret(id);
|
|
364
|
-
return {
|
|
365
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
366
|
-
};
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// src/resources.ts
|
|
371
|
-
var FILTERS_REFERENCE = FilterSchema.options.map((opt) => ({
|
|
372
|
-
type: opt.shape.type.def.values[0],
|
|
373
|
-
fields: Object.keys(opt.shape).filter((k) => k !== "type")
|
|
374
|
-
}));
|
|
375
|
-
var COLUMN_TYPES = [
|
|
376
|
-
{
|
|
377
|
-
type: "uint",
|
|
378
|
-
sqlType: "bigint",
|
|
379
|
-
description: "Unsigned integer (Clarity uint)"
|
|
380
|
-
},
|
|
381
|
-
{
|
|
382
|
-
type: "int",
|
|
383
|
-
sqlType: "bigint",
|
|
384
|
-
description: "Signed integer (Clarity int)"
|
|
385
|
-
},
|
|
386
|
-
{ type: "text", sqlType: "text", description: "UTF-8 string" },
|
|
387
|
-
{
|
|
388
|
-
type: "principal",
|
|
389
|
-
sqlType: "text",
|
|
390
|
-
description: "Stacks address (standard or contract)"
|
|
391
|
-
},
|
|
392
|
-
{ type: "bool", sqlType: "boolean", description: "Boolean value" },
|
|
393
|
-
{ type: "json", sqlType: "jsonb", description: "Arbitrary JSON data" },
|
|
394
|
-
{
|
|
395
|
-
options: ["nullable", "indexed", "search"],
|
|
396
|
-
description: "Column options: nullable allows NULL, indexed creates a B-tree index, search enables full-text search"
|
|
397
|
-
}
|
|
398
|
-
];
|
|
399
|
-
function registerResources(server) {
|
|
400
|
-
server.resource("filters", "secondlayer://filters", { description: "Stream filter types and their available fields" }, async () => ({
|
|
401
|
-
contents: [
|
|
402
|
-
{
|
|
403
|
-
uri: "secondlayer://filters",
|
|
404
|
-
mimeType: "application/json",
|
|
405
|
-
text: JSON.stringify(FILTERS_REFERENCE, null, 2)
|
|
406
|
-
}
|
|
407
|
-
]
|
|
408
|
-
}));
|
|
409
|
-
server.resource("column-types", "secondlayer://column-types", { description: "Subgraph column types, SQL mappings, and options" }, async () => ({
|
|
410
|
-
contents: [
|
|
411
|
-
{
|
|
412
|
-
uri: "secondlayer://column-types",
|
|
413
|
-
mimeType: "application/json",
|
|
414
|
-
text: JSON.stringify(COLUMN_TYPES, null, 2)
|
|
415
|
-
}
|
|
416
|
-
]
|
|
417
|
-
}));
|
|
418
|
-
server.resource("templates", "secondlayer://templates", {
|
|
419
|
-
description: "Available subgraph templates with descriptions and categories"
|
|
420
|
-
}, async () => ({
|
|
421
|
-
contents: [
|
|
422
|
-
{
|
|
423
|
-
uri: "secondlayer://templates",
|
|
424
|
-
mimeType: "application/json",
|
|
425
|
-
text: JSON.stringify(templates.map(({ id, name, description, category }) => ({
|
|
426
|
-
id,
|
|
427
|
-
name,
|
|
428
|
-
description,
|
|
429
|
-
category
|
|
430
|
-
})), null, 2)
|
|
431
|
-
}
|
|
432
|
-
]
|
|
433
|
-
}));
|
|
434
|
-
}
|
|
435
|
-
|
|
436
179
|
// src/tools/account.ts
|
|
437
180
|
function registerAccountTools(server) {
|
|
438
181
|
defineTool(server, "account_whoami", "Show the authenticated account's email and plan.", {}, async () => {
|
|
439
182
|
const result = await apiRequest("GET", "/api/accounts/me");
|
|
440
|
-
return
|
|
441
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
442
|
-
};
|
|
183
|
+
return jsonResponse(result);
|
|
443
184
|
});
|
|
444
185
|
}
|
|
445
186
|
|
|
446
187
|
// src/tools/scaffold.ts
|
|
447
|
-
import {
|
|
448
|
-
|
|
449
|
-
// src/lib/scaffold-generate.ts
|
|
450
|
-
function isAbiBuffer(t) {
|
|
451
|
-
return typeof t === "object" && t !== null && "buff" in t;
|
|
452
|
-
}
|
|
453
|
-
function isAbiStringAscii(t) {
|
|
454
|
-
return typeof t === "object" && t !== null && "string-ascii" in t;
|
|
455
|
-
}
|
|
456
|
-
function isAbiStringUtf8(t) {
|
|
457
|
-
return typeof t === "object" && t !== null && "string-utf8" in t;
|
|
458
|
-
}
|
|
459
|
-
function isAbiOptional(t) {
|
|
460
|
-
return typeof t === "object" && t !== null && "optional" in t;
|
|
461
|
-
}
|
|
462
|
-
function isAbiTuple(t) {
|
|
463
|
-
return typeof t === "object" && t !== null && "tuple" in t;
|
|
464
|
-
}
|
|
465
|
-
function isAbiList(t) {
|
|
466
|
-
return typeof t === "object" && t !== null && "list" in t;
|
|
467
|
-
}
|
|
468
|
-
function isAbiResponse(t) {
|
|
469
|
-
return typeof t === "object" && t !== null && "response" in t;
|
|
470
|
-
}
|
|
471
|
-
function mapType(abiType, nullable) {
|
|
472
|
-
if (typeof abiType === "string") {
|
|
473
|
-
switch (abiType) {
|
|
474
|
-
case "uint128":
|
|
475
|
-
return { type: "uint", nullable };
|
|
476
|
-
case "int128":
|
|
477
|
-
return { type: "int", nullable };
|
|
478
|
-
case "principal":
|
|
479
|
-
case "trait_reference":
|
|
480
|
-
return { type: "principal", nullable };
|
|
481
|
-
case "bool":
|
|
482
|
-
return { type: "boolean", nullable };
|
|
483
|
-
default: {
|
|
484
|
-
const s = abiType;
|
|
485
|
-
if (s.includes("uint"))
|
|
486
|
-
return { type: "uint", nullable };
|
|
487
|
-
if (s.includes("int"))
|
|
488
|
-
return { type: "int", nullable };
|
|
489
|
-
if (s.includes("string") || s.includes("ascii") || s.includes("utf8")) {
|
|
490
|
-
return { type: "text", nullable };
|
|
491
|
-
}
|
|
492
|
-
if (s.includes("buff"))
|
|
493
|
-
return { type: "text", nullable };
|
|
494
|
-
return { type: "jsonb", nullable };
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
if (isAbiBuffer(abiType))
|
|
499
|
-
return { type: "text", nullable };
|
|
500
|
-
if (isAbiStringAscii(abiType) || isAbiStringUtf8(abiType)) {
|
|
501
|
-
return { type: "text", nullable };
|
|
502
|
-
}
|
|
503
|
-
if (isAbiOptional(abiType))
|
|
504
|
-
return mapType(abiType.optional, true);
|
|
505
|
-
if (isAbiList(abiType) || isAbiTuple(abiType))
|
|
506
|
-
return { type: "jsonb", nullable };
|
|
507
|
-
if (isAbiResponse(abiType)) {
|
|
508
|
-
return mapType(abiType.response.ok, nullable);
|
|
509
|
-
}
|
|
510
|
-
return { type: "jsonb", nullable };
|
|
511
|
-
}
|
|
512
|
-
function clarityTypeToSubgraphColumn(abiType) {
|
|
513
|
-
return mapType(abiType, false);
|
|
514
|
-
}
|
|
515
|
-
function toSnake(name) {
|
|
516
|
-
return name.replace(/-/g, "_");
|
|
517
|
-
}
|
|
518
|
-
function toCamel(name) {
|
|
519
|
-
return name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
520
|
-
}
|
|
521
|
-
function buildColumns(args) {
|
|
522
|
-
if (args.length === 0)
|
|
523
|
-
return " _placeholder: { type: 'text' }";
|
|
524
|
-
return args.map((arg) => {
|
|
525
|
-
const mapped = clarityTypeToSubgraphColumn(arg.type);
|
|
526
|
-
const nullable = mapped.nullable ? ", nullable: true" : "";
|
|
527
|
-
return ` ${toSnake(arg.name)}: { type: '${mapped.type}'${nullable} }`;
|
|
528
|
-
}).join(`,
|
|
529
|
-
`);
|
|
530
|
-
}
|
|
531
|
-
function buildInsertCall(tableName, args) {
|
|
532
|
-
if (args.length === 0) {
|
|
533
|
-
return ` ctx.insert('${tableName}', {
|
|
534
|
-
sender: ctx.tx.sender,
|
|
535
|
-
});`;
|
|
536
|
-
}
|
|
537
|
-
const mappings = args.map((arg) => {
|
|
538
|
-
return ` ${toSnake(arg.name)}: event.${toCamel(arg.name)}`;
|
|
539
|
-
});
|
|
540
|
-
return ` ctx.insert('${tableName}', {
|
|
541
|
-
${mappings.join(`,
|
|
542
|
-
`)},
|
|
543
|
-
});`;
|
|
544
|
-
}
|
|
545
|
-
function generateSubgraphCode(contractId, functions, subgraphName, events) {
|
|
546
|
-
const contractParts = contractId.split(".");
|
|
547
|
-
const contractName = contractParts[contractParts.length - 1] ?? contractId;
|
|
548
|
-
const name = subgraphName ?? contractName;
|
|
549
|
-
const publicFunctions = functions.filter((f) => f.access === "public");
|
|
550
|
-
const hasEvents = events && events.length > 0;
|
|
551
|
-
if (publicFunctions.length === 0 && !hasEvents) {
|
|
552
|
-
return `// No public functions or events selected for ${contractId}`;
|
|
553
|
-
}
|
|
554
|
-
const tableDefs = [];
|
|
555
|
-
if (hasEvents) {
|
|
556
|
-
for (const ev of events) {
|
|
557
|
-
const tableName = toSnake(ev.name);
|
|
558
|
-
let columns;
|
|
559
|
-
if (isAbiTuple(ev.value)) {
|
|
560
|
-
columns = buildColumns(ev.value.tuple);
|
|
561
|
-
} else {
|
|
562
|
-
columns = ` value: { type: '${clarityTypeToSubgraphColumn(ev.value).type}' }`;
|
|
563
|
-
}
|
|
564
|
-
tableDefs.push(` ${tableName}: {
|
|
565
|
-
columns: {
|
|
566
|
-
${columns}
|
|
567
|
-
}
|
|
568
|
-
}`);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
for (const fn of publicFunctions) {
|
|
572
|
-
const tableName = toSnake(fn.name);
|
|
573
|
-
const columns = buildColumns(fn.args);
|
|
574
|
-
tableDefs.push(` ${tableName}: {
|
|
575
|
-
columns: {
|
|
576
|
-
${columns}
|
|
577
|
-
}
|
|
578
|
-
}`);
|
|
579
|
-
}
|
|
580
|
-
const schemaBlock = tableDefs.join(`,
|
|
581
|
-
`);
|
|
582
|
-
const sourceEntries = [`{ contract: '${contractId}' }`];
|
|
583
|
-
const handlerEntries = [];
|
|
584
|
-
if (hasEvents) {
|
|
585
|
-
for (const ev of events) {
|
|
586
|
-
const tableName = toSnake(ev.name);
|
|
587
|
-
let insertCall;
|
|
588
|
-
if (isAbiTuple(ev.value)) {
|
|
589
|
-
insertCall = buildInsertCall(tableName, ev.value.tuple);
|
|
590
|
-
} else {
|
|
591
|
-
insertCall = ` ctx.insert('${tableName}', {
|
|
592
|
-
value: event.value,
|
|
593
|
-
});`;
|
|
594
|
-
}
|
|
595
|
-
handlerEntries.push(` '${contractId}::${ev.name}': async (event, ctx) => {
|
|
596
|
-
${insertCall}
|
|
597
|
-
}`);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
for (const fn of publicFunctions) {
|
|
601
|
-
const tableName = toSnake(fn.name);
|
|
602
|
-
const insertCall = buildInsertCall(tableName, fn.args);
|
|
603
|
-
handlerEntries.push(` '${contractId}::${fn.name}': async (event, ctx) => {
|
|
604
|
-
${insertCall}
|
|
605
|
-
}`);
|
|
606
|
-
}
|
|
607
|
-
const handlersBlock = handlerEntries.join(`,
|
|
608
|
-
|
|
609
|
-
`);
|
|
610
|
-
return `import { defineSubgraph } from '@secondlayer/subgraphs';
|
|
611
|
-
|
|
612
|
-
export default defineSubgraph({
|
|
613
|
-
name: '${name}',
|
|
614
|
-
sources: [${sourceEntries.join(", ")}],
|
|
615
|
-
schema: {
|
|
616
|
-
${schemaBlock}
|
|
617
|
-
},
|
|
618
|
-
handlers: {
|
|
619
|
-
${handlersBlock}
|
|
620
|
-
}
|
|
621
|
-
});
|
|
622
|
-
`;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// src/tools/scaffold.ts
|
|
188
|
+
import { generateSubgraphCode } from "@secondlayer/scaffold";
|
|
189
|
+
import { z } from "zod/v4";
|
|
626
190
|
var API_BASE = process.env.SECONDLAYER_API_URL || "https://api.secondlayer.tools";
|
|
627
191
|
async function fetchAbi(contractId) {
|
|
628
192
|
const res = await fetch(`${API_BASE}/api/node/contracts/${contractId}/abi`, {
|
|
@@ -641,17 +205,17 @@ async function fetchAbi(contractId) {
|
|
|
641
205
|
}
|
|
642
206
|
function registerScaffoldTools(server) {
|
|
643
207
|
defineTool(server, "scaffold_from_contract", "Generate a subgraph scaffold from a deployed Stacks contract. Fetches the ABI automatically.", {
|
|
644
|
-
contractId:
|
|
645
|
-
subgraphName:
|
|
208
|
+
contractId: z.string().describe("Fully qualified contract ID (e.g. SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01)"),
|
|
209
|
+
subgraphName: z.string().optional().describe("Override the subgraph name (defaults to contract name)")
|
|
646
210
|
}, async ({ contractId, subgraphName }) => {
|
|
647
211
|
const { functions, maps } = await fetchAbi(contractId);
|
|
648
212
|
const code = generateSubgraphCode(contractId, functions, subgraphName, maps);
|
|
649
213
|
return { content: [{ type: "text", text: code }] };
|
|
650
214
|
});
|
|
651
215
|
defineTool(server, "scaffold_from_abi", "Generate a subgraph scaffold from a provided ABI JSON. Use when you already have the ABI.", {
|
|
652
|
-
abi:
|
|
653
|
-
contractId:
|
|
654
|
-
subgraphName:
|
|
216
|
+
abi: z.string().describe("ABI JSON string (the full contract ABI object)"),
|
|
217
|
+
contractId: z.string().describe("Fully qualified contract ID"),
|
|
218
|
+
subgraphName: z.string().optional().describe("Override the subgraph name")
|
|
655
219
|
}, async ({ abi, contractId, subgraphName }) => {
|
|
656
220
|
let parsed;
|
|
657
221
|
try {
|
|
@@ -668,51 +232,8 @@ function registerScaffoldTools(server) {
|
|
|
668
232
|
}
|
|
669
233
|
|
|
670
234
|
// src/tools/subgraphs.ts
|
|
671
|
-
import {
|
|
672
|
-
|
|
673
|
-
// src/lib/bundle.ts
|
|
674
|
-
import { validateSubgraphDefinition } from "@secondlayer/subgraphs/validate";
|
|
675
|
-
import esbuild from "esbuild";
|
|
676
|
-
async function bundleSubgraphCode(code) {
|
|
677
|
-
let result;
|
|
678
|
-
try {
|
|
679
|
-
result = await esbuild.build({
|
|
680
|
-
stdin: { contents: code, loader: "ts", resolveDir: process.cwd() },
|
|
681
|
-
bundle: true,
|
|
682
|
-
platform: "node",
|
|
683
|
-
format: "esm",
|
|
684
|
-
external: ["@secondlayer/subgraphs"],
|
|
685
|
-
write: false
|
|
686
|
-
});
|
|
687
|
-
} catch (err) {
|
|
688
|
-
throw new Error(`Bundle failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
689
|
-
}
|
|
690
|
-
const handlerCode = new TextDecoder().decode(result.outputFiles[0].contents);
|
|
691
|
-
let mod;
|
|
692
|
-
try {
|
|
693
|
-
const dataUri = `data:text/javascript;base64,${Buffer.from(handlerCode).toString("base64")}`;
|
|
694
|
-
mod = await import(dataUri);
|
|
695
|
-
} catch (err) {
|
|
696
|
-
throw new Error(`Module evaluation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
697
|
-
}
|
|
698
|
-
const def = mod.default ?? mod;
|
|
699
|
-
let validated;
|
|
700
|
-
try {
|
|
701
|
-
validated = validateSubgraphDefinition(def);
|
|
702
|
-
} catch (err) {
|
|
703
|
-
throw new Error(`Validation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
704
|
-
}
|
|
705
|
-
return {
|
|
706
|
-
name: validated.name,
|
|
707
|
-
version: validated.version,
|
|
708
|
-
description: validated.description,
|
|
709
|
-
sources: validated.sources,
|
|
710
|
-
schema: validated.schema,
|
|
711
|
-
handlerCode
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// src/tools/subgraphs.ts
|
|
235
|
+
import { bundleSubgraphCode } from "@secondlayer/bundler";
|
|
236
|
+
import { z as z2 } from "zod/v4";
|
|
716
237
|
function registerSubgraphTools(server) {
|
|
717
238
|
defineTool(server, "subgraphs_list", "List all deployed subgraphs. Returns summary fields only.", {}, async () => {
|
|
718
239
|
const { data } = await getClient().subgraphs.list();
|
|
@@ -725,22 +246,22 @@ function registerSubgraphTools(server) {
|
|
|
725
246
|
]
|
|
726
247
|
};
|
|
727
248
|
});
|
|
728
|
-
defineTool(server, "subgraphs_get", "Get full details of a subgraph including schema, health, and table columns.", { name:
|
|
249
|
+
defineTool(server, "subgraphs_get", "Get full details of a subgraph including schema, health, and table columns.", { name: z2.string().describe("Subgraph name") }, async ({ name }) => {
|
|
729
250
|
const detail = await getClient().subgraphs.get(name);
|
|
730
251
|
return {
|
|
731
252
|
content: [{ type: "text", text: JSON.stringify(detail, null, 2) }]
|
|
732
253
|
};
|
|
733
254
|
});
|
|
734
255
|
defineTool(server, "subgraphs_query", 'Query rows from a subgraph table (max 200 rows). Filters support operators: "amount.gte": "1000", "sender.neq": "SP...", "name.like": "%token%". Available operators: eq, neq, gt, gte, lt, lte, like.', {
|
|
735
|
-
name:
|
|
736
|
-
table:
|
|
737
|
-
filters:
|
|
738
|
-
sort:
|
|
739
|
-
order:
|
|
740
|
-
limit:
|
|
741
|
-
offset:
|
|
742
|
-
fields:
|
|
743
|
-
count:
|
|
256
|
+
name: z2.string().describe("Subgraph name"),
|
|
257
|
+
table: z2.string().describe("Table name"),
|
|
258
|
+
filters: z2.record(z2.string(), z2.string()).optional().describe('Column filters — plain values or with operators (e.g. {"amount.gte": "1000", "sender": "SP..."})'),
|
|
259
|
+
sort: z2.string().optional().describe("Column to sort by"),
|
|
260
|
+
order: z2.enum(["asc", "desc"]).optional().describe("Sort order"),
|
|
261
|
+
limit: z2.number().max(200).optional().describe("Max rows (default 50, max 200)"),
|
|
262
|
+
offset: z2.number().optional().describe("Offset for pagination"),
|
|
263
|
+
fields: z2.string().optional().describe('Comma-separated column list to return (e.g. "sender,amount")'),
|
|
264
|
+
count: z2.boolean().optional().describe("If true, return row count instead of rows")
|
|
744
265
|
}, async ({
|
|
745
266
|
name,
|
|
746
267
|
table,
|
|
@@ -773,9 +294,9 @@ function registerSubgraphTools(server) {
|
|
|
773
294
|
};
|
|
774
295
|
});
|
|
775
296
|
defineTool(server, "subgraphs_reindex", "Reindex a subgraph from a specific block range.", {
|
|
776
|
-
name:
|
|
777
|
-
fromBlock:
|
|
778
|
-
toBlock:
|
|
297
|
+
name: z2.string().describe("Subgraph name"),
|
|
298
|
+
fromBlock: z2.number().optional().describe("Start block (defaults to beginning)"),
|
|
299
|
+
toBlock: z2.number().optional().describe("End block (defaults to latest)")
|
|
779
300
|
}, async ({ name, fromBlock, toBlock }) => {
|
|
780
301
|
const result = await getClient().subgraphs.reindex(name, {
|
|
781
302
|
fromBlock,
|
|
@@ -785,13 +306,13 @@ function registerSubgraphTools(server) {
|
|
|
785
306
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
786
307
|
};
|
|
787
308
|
});
|
|
788
|
-
defineTool(server, "subgraphs_delete", "Delete a subgraph permanently.", { name:
|
|
309
|
+
defineTool(server, "subgraphs_delete", "Delete a subgraph permanently.", { name: z2.string().describe("Subgraph name") }, async ({ name }) => {
|
|
789
310
|
const result = await getClient().subgraphs.delete(name);
|
|
790
311
|
return { content: [{ type: "text", text: result.message }] };
|
|
791
312
|
});
|
|
792
313
|
defineTool(server, "subgraphs_deploy", "Deploy a subgraph from TypeScript code. Pass the full defineSubgraph() source — it will be bundled, validated, and deployed.", {
|
|
793
|
-
code:
|
|
794
|
-
reindex:
|
|
314
|
+
code: z2.string().describe("TypeScript source code containing a defineSubgraph() call"),
|
|
315
|
+
reindex: z2.boolean().optional().describe("Force reindex on breaking schema change (drops and rebuilds all data)")
|
|
795
316
|
}, async ({ code, reindex }) => {
|
|
796
317
|
const bundled = await bundleSubgraphCode(code);
|
|
797
318
|
const result = await getClient().subgraphs.deploy({
|
|
@@ -801,16 +322,29 @@ function registerSubgraphTools(server) {
|
|
|
801
322
|
sources: bundled.sources,
|
|
802
323
|
schema: bundled.schema,
|
|
803
324
|
handlerCode: bundled.handlerCode,
|
|
325
|
+
sourceCode: code,
|
|
804
326
|
reindex
|
|
805
327
|
});
|
|
806
328
|
return {
|
|
807
329
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
808
330
|
};
|
|
809
331
|
});
|
|
332
|
+
defineTool(server, "subgraphs_read_source", "Fetch the deployed TypeScript source of a subgraph (plus its stored version). Returns a readOnly payload for subgraphs deployed before source capture — in that case the caller should redeploy via CLI before editing.", { name: z2.string().describe("Subgraph name") }, async ({ name }) => {
|
|
333
|
+
const source = await getClient().subgraphs.getSource(name);
|
|
334
|
+
return {
|
|
335
|
+
content: [{ type: "text", text: JSON.stringify(source, null, 2) }]
|
|
336
|
+
};
|
|
337
|
+
});
|
|
810
338
|
}
|
|
811
339
|
|
|
812
340
|
// src/tools/workflows.ts
|
|
813
|
-
import {
|
|
341
|
+
import { bundleWorkflowCode } from "@secondlayer/bundler";
|
|
342
|
+
import {
|
|
343
|
+
generateWorkflowCode
|
|
344
|
+
} from "@secondlayer/scaffold";
|
|
345
|
+
import { VersionConflictError } from "@secondlayer/sdk";
|
|
346
|
+
import { createPatch } from "diff";
|
|
347
|
+
import { z as z3 } from "zod/v4";
|
|
814
348
|
function registerWorkflowTools(server) {
|
|
815
349
|
defineTool(server, "workflows_list", "List all workflows. Returns summary fields only.", {}, async () => {
|
|
816
350
|
const { workflows } = await getClient().workflows.list();
|
|
@@ -823,15 +357,155 @@ function registerWorkflowTools(server) {
|
|
|
823
357
|
]
|
|
824
358
|
};
|
|
825
359
|
});
|
|
826
|
-
defineTool(server, "workflows_get", "Get full details of a workflow by name.", { name:
|
|
360
|
+
defineTool(server, "workflows_get", "Get full details of a workflow by name.", { name: z3.string().describe("Workflow name") }, async ({ name }) => {
|
|
827
361
|
const detail = await getClient().workflows.get(name);
|
|
828
362
|
return {
|
|
829
363
|
content: [{ type: "text", text: JSON.stringify(detail, null, 2) }]
|
|
830
364
|
};
|
|
831
365
|
});
|
|
366
|
+
defineTool(server, "workflows_get_definition", "Return the deployed TypeScript source of a workflow plus its stored version. Returns `sourceCode: null` + `readOnly: true` for workflows deployed before source capture.", { name: z3.string().describe("Workflow name") }, async ({ name }) => {
|
|
367
|
+
const source = await getClient().workflows.getSource(name);
|
|
368
|
+
return {
|
|
369
|
+
content: [{ type: "text", text: JSON.stringify(source, null, 2) }]
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
defineTool(server, "workflows_propose_edit", "Validate a proposed edit WITHOUT deploying. Fetches the current stored source, bundles the proposed source, computes a unified diff, and returns everything for review. Use this when you want to show the user a diff before committing — pair it with workflows_deploy(expectedVersion=...) to persist.", {
|
|
373
|
+
name: z3.string().describe("Workflow name"),
|
|
374
|
+
proposedCode: z3.string().describe("New TypeScript source — must compile and validate."),
|
|
375
|
+
expectedVersion: z3.string().regex(/^\d+\.\d+\.\d+$/).optional().describe("Version the proposer is editing from (for audit).")
|
|
376
|
+
}, async ({ name, proposedCode, expectedVersion }) => {
|
|
377
|
+
const current = await getClient().workflows.getSource(name);
|
|
378
|
+
if (current.sourceCode === null) {
|
|
379
|
+
return {
|
|
380
|
+
isError: true,
|
|
381
|
+
content: [
|
|
382
|
+
{
|
|
383
|
+
type: "text",
|
|
384
|
+
text: JSON.stringify({
|
|
385
|
+
error: "Workflow has no stored source. Redeploy via CLI first.",
|
|
386
|
+
readOnly: true,
|
|
387
|
+
version: current.version
|
|
388
|
+
}, null, 2)
|
|
389
|
+
}
|
|
390
|
+
]
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
let bundleValid = false;
|
|
394
|
+
let validation;
|
|
395
|
+
let bundleSize = 0;
|
|
396
|
+
try {
|
|
397
|
+
const bundled = await bundleWorkflowCode(proposedCode);
|
|
398
|
+
bundleValid = true;
|
|
399
|
+
bundleSize = Buffer.byteLength(bundled.handlerCode, "utf8");
|
|
400
|
+
validation = {
|
|
401
|
+
name: bundled.name,
|
|
402
|
+
triggerType: bundled.trigger.type
|
|
403
|
+
};
|
|
404
|
+
} catch (err) {
|
|
405
|
+
validation = {
|
|
406
|
+
error: err instanceof Error ? err.message : String(err)
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
const diffText = createPatch(`${name}.ts`, current.sourceCode, proposedCode, `v${current.version}`, "proposed");
|
|
410
|
+
return {
|
|
411
|
+
content: [
|
|
412
|
+
{
|
|
413
|
+
type: "text",
|
|
414
|
+
text: JSON.stringify({
|
|
415
|
+
currentVersion: current.version,
|
|
416
|
+
expectedVersion,
|
|
417
|
+
currentSource: current.sourceCode,
|
|
418
|
+
proposedSource: proposedCode,
|
|
419
|
+
diffText,
|
|
420
|
+
bundleValid,
|
|
421
|
+
validation,
|
|
422
|
+
bundleSize
|
|
423
|
+
}, null, 2)
|
|
424
|
+
}
|
|
425
|
+
]
|
|
426
|
+
};
|
|
427
|
+
});
|
|
428
|
+
defineTool(server, "workflows_tail_run", "Tail a workflow run via SSE and return a compacted log. Resolves as soon as the run completes, `limit` events are collected, or `timeoutMs` elapses (default 60s). MCP is not streaming-first — use this for short-lived follow-ups, not long tails.", {
|
|
429
|
+
name: z3.string().describe("Workflow name"),
|
|
430
|
+
runId: z3.string().describe("Run id"),
|
|
431
|
+
limit: z3.number().int().positive().max(200).optional().describe("Max step events to collect (default 50)"),
|
|
432
|
+
timeoutMs: z3.number().int().positive().max(5 * 60 * 1000).optional().describe("Hard timeout in ms (default 60000, max 300000)")
|
|
433
|
+
}, async ({ name, runId, limit, timeoutMs }) => {
|
|
434
|
+
const cap = limit ?? 50;
|
|
435
|
+
const deadline = timeoutMs ?? 60000;
|
|
436
|
+
const events = [];
|
|
437
|
+
let finalStatus = null;
|
|
438
|
+
let stoppedBy = "timeout";
|
|
439
|
+
const controller = new AbortController;
|
|
440
|
+
const timer = setTimeout(() => {
|
|
441
|
+
stoppedBy = "timeout";
|
|
442
|
+
controller.abort();
|
|
443
|
+
}, deadline);
|
|
444
|
+
try {
|
|
445
|
+
await getClient().workflows.streamRun(name, runId, (event) => {
|
|
446
|
+
if (event.type === "step") {
|
|
447
|
+
events.push(event.step);
|
|
448
|
+
if (events.length >= cap) {
|
|
449
|
+
stoppedBy = "limit";
|
|
450
|
+
controller.abort();
|
|
451
|
+
}
|
|
452
|
+
} else if (event.type === "done") {
|
|
453
|
+
finalStatus = event.done.status;
|
|
454
|
+
stoppedBy = "done";
|
|
455
|
+
}
|
|
456
|
+
}, controller.signal);
|
|
457
|
+
} catch (err) {
|
|
458
|
+
if (!(err instanceof Error) || err.name !== "AbortError") {
|
|
459
|
+
throw err;
|
|
460
|
+
}
|
|
461
|
+
} finally {
|
|
462
|
+
clearTimeout(timer);
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
content: [
|
|
466
|
+
{
|
|
467
|
+
type: "text",
|
|
468
|
+
text: JSON.stringify({
|
|
469
|
+
runId,
|
|
470
|
+
finalStatus,
|
|
471
|
+
stoppedBy,
|
|
472
|
+
eventCount: events.length,
|
|
473
|
+
events
|
|
474
|
+
}, null, 2)
|
|
475
|
+
}
|
|
476
|
+
]
|
|
477
|
+
};
|
|
478
|
+
});
|
|
479
|
+
defineTool(server, "workflows_pause_all", "Pause ALL active workflows for the authenticated account. Irreversible only by calling pause/resume per workflow. Returns the list of affected workflows.", {}, async () => {
|
|
480
|
+
const result = await getClient().workflows.pauseAll();
|
|
481
|
+
return {
|
|
482
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
defineTool(server, "workflows_cancel_run", "Cancel an in-flight workflow run. Marks the run as cancelled and removes any pending queue entry. No-ops if the run is already terminal.", { runId: z3.string().describe("Run id to cancel") }, async ({ runId }) => {
|
|
486
|
+
const result = await getClient().workflows.cancelRun(runId);
|
|
487
|
+
return {
|
|
488
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
489
|
+
};
|
|
490
|
+
});
|
|
491
|
+
defineTool(server, "workflows_rollback", "Roll a workflow back to a prior version. The restored handler is re-published as a NEW version (audit trail), so no history is lost. Pass toVersion to pick a specific bundle; omit to roll back to the immediate previous version on disk. Last 3 versions are retained.", {
|
|
492
|
+
name: z3.string().describe("Workflow name"),
|
|
493
|
+
toVersion: z3.string().regex(/^\d+\.\d+\.\d+$/).optional().describe("Target version to restore. Must be one of the retained bundles on disk.")
|
|
494
|
+
}, async ({ name, toVersion }) => {
|
|
495
|
+
const result = await getClient().workflows.rollback(name, toVersion);
|
|
496
|
+
return {
|
|
497
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
498
|
+
};
|
|
499
|
+
});
|
|
500
|
+
defineTool(server, "workflows_delete", "Delete a workflow permanently.", { name: z3.string().describe("Workflow name") }, async ({ name }) => {
|
|
501
|
+
await getClient().workflows.delete(name);
|
|
502
|
+
return {
|
|
503
|
+
content: [{ type: "text", text: `Deleted workflow "${name}"` }]
|
|
504
|
+
};
|
|
505
|
+
});
|
|
832
506
|
defineTool(server, "workflows_trigger", "Trigger a workflow run. Optionally pass input as a JSON string.", {
|
|
833
|
-
name:
|
|
834
|
-
input:
|
|
507
|
+
name: z3.string().describe("Workflow name"),
|
|
508
|
+
input: z3.string().optional().describe("Input as JSON string")
|
|
835
509
|
}, async ({ name, input }) => {
|
|
836
510
|
const parsed = input ? JSON.parse(input) : undefined;
|
|
837
511
|
const result = await getClient().workflows.trigger(name, parsed);
|
|
@@ -839,89 +513,110 @@ function registerWorkflowTools(server) {
|
|
|
839
513
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
840
514
|
};
|
|
841
515
|
});
|
|
842
|
-
defineTool(server, "workflows_pause", "Pause a running workflow.", { name:
|
|
516
|
+
defineTool(server, "workflows_pause", "Pause a running workflow.", { name: z3.string().describe("Workflow name") }, async ({ name }) => {
|
|
843
517
|
await getClient().workflows.pause(name);
|
|
844
518
|
return {
|
|
845
519
|
content: [{ type: "text", text: `Paused workflow "${name}"` }]
|
|
846
520
|
};
|
|
847
521
|
});
|
|
848
|
-
defineTool(server, "workflows_resume", "Resume a paused workflow.", { name:
|
|
522
|
+
defineTool(server, "workflows_resume", "Resume a paused workflow.", { name: z3.string().describe("Workflow name") }, async ({ name }) => {
|
|
849
523
|
await getClient().workflows.resume(name);
|
|
850
524
|
return {
|
|
851
525
|
content: [{ type: "text", text: `Resumed workflow "${name}"` }]
|
|
852
526
|
};
|
|
853
527
|
});
|
|
854
|
-
defineTool(server, "
|
|
855
|
-
name:
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
528
|
+
defineTool(server, "workflows_scaffold", "Generate a compilable defineWorkflow() skeleton from a typed intent. Returns the TypeScript source; pass it to workflows_deploy to persist. Placeholders inside the source must be filled in before running a real workflow.", {
|
|
529
|
+
name: z3.string().regex(/^[a-z][a-z0-9-]*$/).describe("Workflow name (lowercase, hyphens)"),
|
|
530
|
+
trigger: z3.discriminatedUnion("type", [
|
|
531
|
+
z3.object({
|
|
532
|
+
type: z3.literal("event"),
|
|
533
|
+
filterType: z3.string().optional()
|
|
534
|
+
}),
|
|
535
|
+
z3.object({
|
|
536
|
+
type: z3.literal("schedule"),
|
|
537
|
+
cron: z3.string().min(1),
|
|
538
|
+
timezone: z3.string().optional()
|
|
539
|
+
}),
|
|
540
|
+
z3.object({ type: z3.literal("manual") })
|
|
541
|
+
]).describe("Trigger shape"),
|
|
542
|
+
steps: z3.array(z3.enum(["run", "query", "ai", "deliver"])).describe("Ordered list of step kinds to include in the handler"),
|
|
543
|
+
deliveryTarget: z3.enum(["webhook", "slack", "email", "discord", "telegram"]).optional().describe("Delivery target used when steps includes `deliver`")
|
|
544
|
+
}, async ({ name, trigger, steps, deliveryTarget }) => {
|
|
545
|
+
const code = generateWorkflowCode({
|
|
546
|
+
name,
|
|
547
|
+
trigger,
|
|
548
|
+
steps,
|
|
549
|
+
deliveryTarget
|
|
862
550
|
});
|
|
863
|
-
return {
|
|
864
|
-
content: [
|
|
865
|
-
{
|
|
866
|
-
type: "text",
|
|
867
|
-
text: JSON.stringify(runs, null, 2)
|
|
868
|
-
}
|
|
869
|
-
]
|
|
870
|
-
};
|
|
871
|
-
});
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
// src/tools/templates.ts
|
|
875
|
-
import {
|
|
876
|
-
getTemplateById,
|
|
877
|
-
getTemplatesByCategory,
|
|
878
|
-
templates as templates2
|
|
879
|
-
} from "@secondlayer/subgraphs/templates";
|
|
880
|
-
import { z as z5 } from "zod/v4";
|
|
881
|
-
function registerTemplateTools(server) {
|
|
882
|
-
defineTool(server, "templates_list", "List available subgraph templates. Returns metadata only — use templates_get for full code.", {
|
|
883
|
-
category: z5.enum(["defi", "nft", "token", "infrastructure"]).optional().describe("Filter by category")
|
|
884
|
-
}, async ({ category }) => {
|
|
885
|
-
const list = category ? getTemplatesByCategory(category) : templates2;
|
|
886
|
-
return {
|
|
887
|
-
content: [
|
|
888
|
-
{
|
|
889
|
-
type: "text",
|
|
890
|
-
text: JSON.stringify(list.map((t) => ({
|
|
891
|
-
id: t.id,
|
|
892
|
-
name: t.name,
|
|
893
|
-
description: t.description,
|
|
894
|
-
category: t.category
|
|
895
|
-
})), null, 2)
|
|
896
|
-
}
|
|
897
|
-
]
|
|
898
|
-
};
|
|
551
|
+
return { content: [{ type: "text", text: code }] };
|
|
899
552
|
});
|
|
900
|
-
defineTool(server, "
|
|
901
|
-
|
|
902
|
-
|
|
553
|
+
defineTool(server, "workflows_deploy", "Deploy a workflow from TypeScript source. Pass the full defineWorkflow() source — it will be bundled, validated, and deployed. Use expectedVersion for optimistic concurrency, or dryRun to validate without persisting.", {
|
|
554
|
+
code: z3.string().describe("TypeScript source code containing a defineWorkflow() call"),
|
|
555
|
+
expectedVersion: z3.string().regex(/^\d+\.\d+\.\d+$/).optional().describe("Stored version the client expects (major.minor.patch). Server returns 409 on mismatch."),
|
|
556
|
+
dryRun: z3.boolean().optional().describe("If true, validate and bundle only — do not persist.")
|
|
557
|
+
}, async ({ code, expectedVersion, dryRun }) => {
|
|
558
|
+
let bundled;
|
|
559
|
+
try {
|
|
560
|
+
bundled = await bundleWorkflowCode(code);
|
|
561
|
+
} catch (err) {
|
|
903
562
|
return {
|
|
563
|
+
isError: true,
|
|
904
564
|
content: [
|
|
905
565
|
{
|
|
906
566
|
type: "text",
|
|
907
|
-
text:
|
|
567
|
+
text: err instanceof Error ? err.message : String(err)
|
|
908
568
|
}
|
|
909
|
-
]
|
|
910
|
-
isError: true
|
|
569
|
+
]
|
|
911
570
|
};
|
|
912
571
|
}
|
|
572
|
+
const base = {
|
|
573
|
+
name: bundled.name,
|
|
574
|
+
trigger: bundled.trigger,
|
|
575
|
+
handlerCode: bundled.handlerCode,
|
|
576
|
+
sourceCode: bundled.sourceCode,
|
|
577
|
+
retries: bundled.retries,
|
|
578
|
+
timeout: bundled.timeout,
|
|
579
|
+
expectedVersion
|
|
580
|
+
};
|
|
581
|
+
try {
|
|
582
|
+
const result = dryRun ? await getClient().workflows.deploy({ ...base, dryRun: true }) : await getClient().workflows.deploy(base);
|
|
583
|
+
return {
|
|
584
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
585
|
+
};
|
|
586
|
+
} catch (err) {
|
|
587
|
+
if (err instanceof VersionConflictError) {
|
|
588
|
+
return {
|
|
589
|
+
isError: true,
|
|
590
|
+
content: [
|
|
591
|
+
{
|
|
592
|
+
type: "text",
|
|
593
|
+
text: JSON.stringify({
|
|
594
|
+
error: err.message,
|
|
595
|
+
code: "VERSION_CONFLICT",
|
|
596
|
+
currentVersion: err.currentVersion,
|
|
597
|
+
expectedVersion: err.expectedVersion
|
|
598
|
+
}, null, 2)
|
|
599
|
+
}
|
|
600
|
+
]
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
throw err;
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
defineTool(server, "workflows_runs", "List runs for a workflow. Optionally filter by status and limit results.", {
|
|
607
|
+
name: z3.string().describe("Workflow name"),
|
|
608
|
+
status: z3.enum(["running", "completed", "failed", "cancelled"]).optional().describe("Filter by run status"),
|
|
609
|
+
limit: z3.number().optional().describe("Max runs to return (default 20)")
|
|
610
|
+
}, async ({ name, status, limit }) => {
|
|
611
|
+
const { runs } = await getClient().workflows.listRuns(name, {
|
|
612
|
+
status,
|
|
613
|
+
limit
|
|
614
|
+
});
|
|
913
615
|
return {
|
|
914
616
|
content: [
|
|
915
617
|
{
|
|
916
618
|
type: "text",
|
|
917
|
-
text: JSON.stringify(
|
|
918
|
-
id: template.id,
|
|
919
|
-
name: template.name,
|
|
920
|
-
description: template.description,
|
|
921
|
-
category: template.category,
|
|
922
|
-
code: template.code,
|
|
923
|
-
prompt: template.prompt
|
|
924
|
-
}, null, 2)
|
|
619
|
+
text: JSON.stringify(runs, null, 2)
|
|
925
620
|
}
|
|
926
621
|
]
|
|
927
622
|
};
|
|
@@ -936,9 +631,7 @@ function createServer() {
|
|
|
936
631
|
name: "secondlayer",
|
|
937
632
|
version: pkg.version
|
|
938
633
|
});
|
|
939
|
-
registerTemplateTools(server);
|
|
940
634
|
registerScaffoldTools(server);
|
|
941
|
-
registerStreamTools(server);
|
|
942
635
|
registerSubgraphTools(server);
|
|
943
636
|
registerAccountTools(server);
|
|
944
637
|
registerWorkflowTools(server);
|
|
@@ -1032,5 +725,5 @@ httpServer.listen(port, () => {
|
|
|
1032
725
|
console.error("Warning: SECONDLAYER_MCP_SECRET not set, authentication disabled");
|
|
1033
726
|
});
|
|
1034
727
|
|
|
1035
|
-
//# debugId=
|
|
728
|
+
//# debugId=35A7923DF55CDD2864756E2164756E21
|
|
1036
729
|
//# sourceMappingURL=bin-http.js.map
|