@secondlayer/mcp 11.6.0 → 11.6.2
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/dist/bin-http.js +164 -98
- package/dist/bin-http.js.map +9 -7
- package/dist/bin.js +89 -84
- package/dist/bin.js.map +7 -6
- package/dist/index.js +89 -84
- package/dist/index.js.map +7 -6
- package/package.json +4 -4
package/dist/bin-http.js
CHANGED
|
@@ -6,6 +6,53 @@ import {
|
|
|
6
6
|
} from "node:http";
|
|
7
7
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
8
8
|
|
|
9
|
+
// src/lib/http-sessions.ts
|
|
10
|
+
var SESSION_IDLE_TTL_MS = 30 * 60 * 1000;
|
|
11
|
+
var SESSION_SWEEP_INTERVAL_MS = 60 * 1000;
|
|
12
|
+
|
|
13
|
+
class SessionRegistry {
|
|
14
|
+
ttlMs;
|
|
15
|
+
now;
|
|
16
|
+
entries = new Map;
|
|
17
|
+
constructor(ttlMs = SESSION_IDLE_TTL_MS, now = Date.now) {
|
|
18
|
+
this.ttlMs = ttlMs;
|
|
19
|
+
this.now = now;
|
|
20
|
+
}
|
|
21
|
+
add(id, transport) {
|
|
22
|
+
this.entries.set(id, { transport, lastSeen: this.now() });
|
|
23
|
+
}
|
|
24
|
+
get(id) {
|
|
25
|
+
if (!id)
|
|
26
|
+
return;
|
|
27
|
+
const entry = this.entries.get(id);
|
|
28
|
+
if (!entry)
|
|
29
|
+
return;
|
|
30
|
+
entry.lastSeen = this.now();
|
|
31
|
+
return entry.transport;
|
|
32
|
+
}
|
|
33
|
+
delete(id) {
|
|
34
|
+
this.entries.delete(id);
|
|
35
|
+
}
|
|
36
|
+
get size() {
|
|
37
|
+
return this.entries.size;
|
|
38
|
+
}
|
|
39
|
+
async sweep() {
|
|
40
|
+
const cutoff = this.now() - this.ttlMs;
|
|
41
|
+
const expired = [...this.entries].filter(([, entry]) => entry.lastSeen <= cutoff);
|
|
42
|
+
for (const [id] of expired)
|
|
43
|
+
this.entries.delete(id);
|
|
44
|
+
await Promise.allSettled(expired.map(([, entry]) => entry.transport.close()));
|
|
45
|
+
return expired.map(([id]) => id);
|
|
46
|
+
}
|
|
47
|
+
startSweeping(intervalMs = SESSION_SWEEP_INTERVAL_MS) {
|
|
48
|
+
const timer = setInterval(() => {
|
|
49
|
+
this.sweep();
|
|
50
|
+
}, intervalMs);
|
|
51
|
+
timer.unref?.();
|
|
52
|
+
return () => clearInterval(timer);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
9
56
|
// src/server.ts
|
|
10
57
|
import { readFileSync } from "node:fs";
|
|
11
58
|
import { dirname, join as join2 } from "node:path";
|
|
@@ -107,11 +154,11 @@ import {
|
|
|
107
154
|
isHostedApiUrl,
|
|
108
155
|
resolveAccountKey,
|
|
109
156
|
resolveApiKey,
|
|
110
|
-
resolveBaseUrl
|
|
157
|
+
resolveBaseUrl,
|
|
158
|
+
resolveIdentity
|
|
111
159
|
} from "@secondlayer/sdk";
|
|
112
160
|
var instance = null;
|
|
113
161
|
var HOSTED_KEY_HINT = "set SECONDLAYER_API_KEY (sk-sl_*) for credits; INSTANCE_TOKEN is the instance";
|
|
114
|
-
var DEFAULT_ARCHIVE_OPS_URL = "https://api.secondlayer.tools";
|
|
115
162
|
function readApiKey() {
|
|
116
163
|
return resolveApiKey(undefined, resolveBaseUrl());
|
|
117
164
|
}
|
|
@@ -129,19 +176,15 @@ function getClient() {
|
|
|
129
176
|
}
|
|
130
177
|
return instance;
|
|
131
178
|
}
|
|
132
|
-
function readArchiveApiKey() {
|
|
133
|
-
return resolveAccountKey();
|
|
134
|
-
}
|
|
135
179
|
function getArchiveOpsClient() {
|
|
136
|
-
const
|
|
137
|
-
if (!
|
|
180
|
+
const { token, url } = resolveIdentity("account");
|
|
181
|
+
if (!token) {
|
|
138
182
|
throw new Error(HOSTED_KEY_HINT);
|
|
139
183
|
}
|
|
140
|
-
const archiveOpsUrl = process.env.SL_CREDITS_API_URL || process.env.ARCHIVE_OPS_API_URL || DEFAULT_ARCHIVE_OPS_URL;
|
|
141
184
|
return new SecondLayer({
|
|
142
|
-
accountKey,
|
|
185
|
+
accountKey: token,
|
|
143
186
|
origin: "mcp",
|
|
144
|
-
archiveOpsUrl
|
|
187
|
+
archiveOpsUrl: url
|
|
145
188
|
});
|
|
146
189
|
}
|
|
147
190
|
function keyHint(baseUrl = resolveBaseUrl()) {
|
|
@@ -876,7 +919,16 @@ import { DECODED_EVENT_TYPES as DECODED_EVENT_TYPES2 } from "@secondlayer/shared
|
|
|
876
919
|
import {
|
|
877
920
|
runSubgraphTest
|
|
878
921
|
} from "@secondlayer/subgraphs/testing";
|
|
922
|
+
import { z as z10 } from "zod";
|
|
923
|
+
|
|
924
|
+
// src/lib/ids.ts
|
|
879
925
|
import { z as z9 } from "zod";
|
|
926
|
+
var uuidSchema = z9.string().uuid();
|
|
927
|
+
var webhookIdSchema = uuidSchema.describe("Webhook id");
|
|
928
|
+
var subgraphNameSchema = z9.string().min(1).max(63).regex(/^[a-z0-9-]+$/, "lowercase alphanumeric + hyphens only").describe("Subgraph name");
|
|
929
|
+
var tableNameSchema = z9.string().min(1).max(63).regex(/^[a-z_][a-z0-9_]*$/i, "letters, digits and underscores only").describe("Table name");
|
|
930
|
+
|
|
931
|
+
// src/tools/subgraphs.ts
|
|
880
932
|
function eventTypeFor(filter) {
|
|
881
933
|
if (filter.type === "contract_deploy")
|
|
882
934
|
return null;
|
|
@@ -902,13 +954,13 @@ function registerSubgraphTools(server, clientProvider = getClient) {
|
|
|
902
954
|
const { data } = await clientProvider().subgraphs.list();
|
|
903
955
|
return jsonResponse(data.map(formatSubgraphSummary));
|
|
904
956
|
});
|
|
905
|
-
defineTool(server, "subgraphs_status", "Get full details of a subgraph including schema, health (incl. health.emptyMapping when processed events wrote 0 rows), and table columns.", { name:
|
|
957
|
+
defineTool(server, "subgraphs_status", "Get full details of a subgraph including schema, health (incl. health.emptyMapping when processed events wrote 0 rows), and table columns.", { name: subgraphNameSchema }, async ({ name }) => {
|
|
906
958
|
const detail = await clientProvider().subgraphs.status(name);
|
|
907
959
|
return jsonResponse(detail);
|
|
908
960
|
});
|
|
909
961
|
defineTool(server, "subgraphs_spec", "Fetch a subgraph's self-describing spec — the tables, columns, filters, and read URLs a consumer needs, without guessing. `agent` (default) returns the compact JSON schema built for tool use, `openapi` the OpenAPI document for the subgraph's read routes, `markdown` human-readable docs. Read it before writing queries against an unfamiliar subgraph.", {
|
|
910
|
-
name:
|
|
911
|
-
format:
|
|
962
|
+
name: subgraphNameSchema,
|
|
963
|
+
format: z10.enum(["agent", "openapi", "markdown"]).optional().describe("agent = JSON schema for tool use (default), openapi = OpenAPI JSON, markdown = docs.md")
|
|
912
964
|
}, async ({ name, format }) => {
|
|
913
965
|
const client = clientProvider();
|
|
914
966
|
if (format === "openapi")
|
|
@@ -918,15 +970,15 @@ function registerSubgraphTools(server, clientProvider = getClient) {
|
|
|
918
970
|
return jsonResponse(await client.subgraphs.schema(name));
|
|
919
971
|
});
|
|
920
972
|
defineTool(server, "subgraphs_query", 'Query rows from a subgraph table (max 200 rows), reading the open /v1 surface — { rows, next_cursor, tip } envelope, cursor-paginated (no offset). Filters support operators: "amount.gte": "1000", "sender.neq": "SP...", "name.like": "%token%". Available operators: eq, neq, gt, gte, lt, lte, like. Every page with rows returns next_cursor (null only when empty): pass it back as `cursor` to read the next page. To TAIL new rows (no streaming over MCP), poll with cursor = next_cursor ?? the cursor you sent. Fetch one row by id with {"_id": "<id>"}. The same rows are readable over HTTP at GET /v1/subgraphs/<name>/<table>; hand that URL to third parties.', {
|
|
921
|
-
name:
|
|
922
|
-
table:
|
|
923
|
-
filters:
|
|
924
|
-
sort:
|
|
925
|
-
order:
|
|
926
|
-
limit:
|
|
927
|
-
cursor:
|
|
928
|
-
fields:
|
|
929
|
-
count:
|
|
973
|
+
name: subgraphNameSchema,
|
|
974
|
+
table: tableNameSchema,
|
|
975
|
+
filters: z10.record(z10.string(), z10.string()).optional().describe('Column filters — plain values or with operators (e.g. {"amount.gte": "1000", "sender": "SP..."})'),
|
|
976
|
+
sort: z10.string().optional().describe("Column to sort by"),
|
|
977
|
+
order: z10.enum(["asc", "desc"]).optional().describe("Sort order"),
|
|
978
|
+
limit: z10.number().max(200).optional().describe("Max rows (default 50, max 200)"),
|
|
979
|
+
cursor: z10.string().optional().describe("Resume from a previous response's nextCursor"),
|
|
980
|
+
fields: z10.string().optional().describe('Comma-separated column list to return (e.g. "sender,amount")'),
|
|
981
|
+
count: z10.boolean().optional().describe("If true, return row count instead of rows")
|
|
930
982
|
}, async ({
|
|
931
983
|
name,
|
|
932
984
|
table,
|
|
@@ -956,15 +1008,15 @@ function registerSubgraphTools(server, clientProvider = getClient) {
|
|
|
956
1008
|
return jsonResponse({ ...capped, nextCursor: next_cursor, tip });
|
|
957
1009
|
});
|
|
958
1010
|
defineTool(server, "subgraphs_reindex", "Reindex a subgraph: drops and rebuilds the whole subgraph from its start block to chain tip. Takes no block range — use subgraphs_backfill to process a specific range. Returns an operationId — check subgraphs_status (health) or the REST operations endpoint to track progress to completion.", {
|
|
959
|
-
name:
|
|
1011
|
+
name: subgraphNameSchema
|
|
960
1012
|
}, async ({ name }) => {
|
|
961
1013
|
const result = await clientProvider().subgraphs.reindex(name);
|
|
962
1014
|
return jsonResponse(result);
|
|
963
1015
|
});
|
|
964
1016
|
defineTool(server, "subgraphs_backfill", "Backfill a subgraph over a block range. Non-destructive forward fill (does not drop existing data) — unlike subgraphs_reindex, and the only data-fill path for BYO subgraphs (reindex is blocked there). Both blocks required. Returns an operationId — check subgraphs_status (health) or the REST operations endpoint to track progress.", {
|
|
965
|
-
name:
|
|
966
|
-
fromBlock:
|
|
967
|
-
toBlock:
|
|
1017
|
+
name: subgraphNameSchema,
|
|
1018
|
+
fromBlock: z10.number().int().nonnegative().describe("Start block (inclusive)"),
|
|
1019
|
+
toBlock: z10.number().int().nonnegative().describe("End block (inclusive)")
|
|
968
1020
|
}, async ({ name, fromBlock, toBlock }) => {
|
|
969
1021
|
const result = await clientProvider().subgraphs.backfill(name, {
|
|
970
1022
|
fromBlock,
|
|
@@ -972,15 +1024,15 @@ function registerSubgraphTools(server, clientProvider = getClient) {
|
|
|
972
1024
|
});
|
|
973
1025
|
return jsonResponse(result);
|
|
974
1026
|
});
|
|
975
|
-
defineTool(server, "subgraphs_stop", "Cancel an in-flight reindex or backfill operation for a subgraph. Returns the stop request status; check subgraphs_status (health) or the REST operations endpoint to confirm it reaches a terminal state.", { name:
|
|
1027
|
+
defineTool(server, "subgraphs_stop", "Cancel an in-flight reindex or backfill operation for a subgraph. Returns the stop request status; check subgraphs_status (health) or the REST operations endpoint to confirm it reaches a terminal state.", { name: subgraphNameSchema }, async ({ name }) => {
|
|
976
1028
|
const result = await clientProvider().subgraphs.stop(name);
|
|
977
1029
|
return jsonResponse(result);
|
|
978
1030
|
});
|
|
979
1031
|
defineTool(server, "subgraphs_gaps", "List indexing gaps (missing block ranges) for a subgraph. Each gap reports start/end/size, reason, and detected/resolved timestamps. Feed an unresolved gap's range into subgraphs_backfill to fill it. Defaults to unresolved gaps.", {
|
|
980
|
-
name:
|
|
981
|
-
limit:
|
|
982
|
-
offset:
|
|
983
|
-
resolved:
|
|
1032
|
+
name: subgraphNameSchema,
|
|
1033
|
+
limit: z10.number().int().positive().optional().describe("Max gaps to return"),
|
|
1034
|
+
offset: z10.number().int().nonnegative().optional().describe("Pagination offset"),
|
|
1035
|
+
resolved: z10.boolean().optional().describe("Filter by resolved state (omit for unresolved only)")
|
|
984
1036
|
}, async ({ name, limit, offset, resolved }) => {
|
|
985
1037
|
const result = await clientProvider().subgraphs.gaps(name, {
|
|
986
1038
|
limit,
|
|
@@ -990,20 +1042,20 @@ function registerSubgraphTools(server, clientProvider = getClient) {
|
|
|
990
1042
|
return jsonResponse(result);
|
|
991
1043
|
});
|
|
992
1044
|
defineTool(server, "subgraphs_operations", "Operation history for a subgraph (deploy/reindex/backfill/stop), newest first — status, block range, progress, and error. THIS IS THE VERIFY CALL: after subgraphs_deploy, subgraphs_reindex, subgraphs_backfill, or subgraphs_stop, poll it with the returned operationId until `status` is terminal (completed/failed/cancelled) before reporting success. Omit operationId for the recent history.", {
|
|
993
|
-
name:
|
|
994
|
-
operationId:
|
|
1045
|
+
name: subgraphNameSchema,
|
|
1046
|
+
operationId: z10.string().optional().describe("Fetch one operation by id (the operationId returned by reindex/backfill/stop); omit for recent history")
|
|
995
1047
|
}, async ({ name, operationId }) => {
|
|
996
1048
|
const client = clientProvider();
|
|
997
1049
|
return jsonResponse(operationId ? await client.subgraphs.getOperation(name, operationId) : await client.subgraphs.operations(name));
|
|
998
1050
|
});
|
|
999
|
-
defineTool(server, "subgraphs_delete", "Delete a subgraph permanently.", { name:
|
|
1051
|
+
defineTool(server, "subgraphs_delete", "Delete a subgraph permanently.", { name: subgraphNameSchema }, async ({ name }) => {
|
|
1000
1052
|
const result = await clientProvider().subgraphs.delete(name);
|
|
1001
1053
|
return textResponse(result.message);
|
|
1002
1054
|
});
|
|
1003
1055
|
defineTool(server, "subgraphs_deploy", "Deploy a subgraph from TypeScript code. Pass the full defineSubgraph() source — it will be bundled, validated, and deployed. Optional startBlock overrides the source definition for this deploy. dryRun validates schema/DDL only and does not run handlers — call subgraphs_test first and deploy only when it returns ok: true with written >= 1. Call subgraphs_reindex separately if you need a forced reindex.", {
|
|
1004
|
-
code:
|
|
1005
|
-
startBlock:
|
|
1006
|
-
dryRun:
|
|
1056
|
+
code: z10.string().describe("TypeScript source code containing a defineSubgraph() call"),
|
|
1057
|
+
startBlock: z10.number().int().nonnegative().optional().describe("Override the definition startBlock for this deploy"),
|
|
1058
|
+
dryRun: z10.boolean().optional().describe("Validate schema/DDL only — does not run handlers. Use subgraphs_test to prove a mapping writes rows.")
|
|
1007
1059
|
}, async ({ code, startBlock, dryRun }) => {
|
|
1008
1060
|
const bundled = await bundleSubgraphCode(code);
|
|
1009
1061
|
const result = await clientProvider().subgraphs.deploy({
|
|
@@ -1019,10 +1071,10 @@ function registerSubgraphTools(server, clientProvider = getClient) {
|
|
|
1019
1071
|
return jsonResponse(result);
|
|
1020
1072
|
});
|
|
1021
1073
|
defineTool(server, "subgraphs_test", "Prove a subgraph mapping writes rows against Index data before deploy. Pass full defineSubgraph() source plus a height range. Returns ok/written/tables; EMPTY_MAPPING means events matched but 0 rows were written — read hint for observed event.data keys and do not invent fields. subgraphs_deploy dryRun is DDL only; do not deploy until ok: true with written >= 1.", {
|
|
1022
|
-
code:
|
|
1023
|
-
fromHeight:
|
|
1024
|
-
toHeight:
|
|
1025
|
-
limit:
|
|
1074
|
+
code: z10.string().describe("TypeScript source containing a defineSubgraph() call"),
|
|
1075
|
+
fromHeight: z10.number().int().nonnegative().describe("Start block height (inclusive)"),
|
|
1076
|
+
toHeight: z10.number().int().nonnegative().optional().describe("End block height (inclusive); default fromHeight+100"),
|
|
1077
|
+
limit: z10.number().int().positive().max(200).optional().describe("Max Index rows per source (default 50, max 200)")
|
|
1026
1078
|
}, async ({ code, fromHeight, toHeight, limit }) => {
|
|
1027
1079
|
const bundled = await bundleSubgraphCode(code);
|
|
1028
1080
|
const def = await loadBundledDefinition(bundled.handlerCode);
|
|
@@ -1103,36 +1155,36 @@ function registerSubgraphTools(server, clientProvider = getClient) {
|
|
|
1103
1155
|
|
|
1104
1156
|
// src/tools/webhooks.ts
|
|
1105
1157
|
import { CHAIN_TRIGGER_TYPES } from "@secondlayer/shared";
|
|
1106
|
-
import { z as
|
|
1158
|
+
import { z as z11 } from "zod";
|
|
1107
1159
|
function registerWebhookTools(server, clientProvider = getClient) {
|
|
1108
1160
|
defineTool(server, "webhooks_list", "List all webhooks for the current account. Returns summary fields (no secrets).", {}, async () => {
|
|
1109
1161
|
const { data } = await clientProvider().webhooks.list();
|
|
1110
1162
|
return jsonResponse(data);
|
|
1111
1163
|
});
|
|
1112
|
-
defineTool(server, "webhooks_get", "Get full detail for a webhook (triggers, auth, retry config, circuit state).", { id:
|
|
1164
|
+
defineTool(server, "webhooks_get", "Get full detail for a webhook (triggers, auth, retry config, circuit state).", { id: webhookIdSchema }, async ({ id }) => {
|
|
1113
1165
|
const detail = await clientProvider().webhooks.get(id);
|
|
1114
1166
|
return jsonResponse(detail);
|
|
1115
1167
|
});
|
|
1116
1168
|
defineTool(server, "webhooks_create", "Create a webhook that fires on raw chain events (set triggers). Returns `signingSecret` ONCE — forward it to the user so they can wire it into their receiver.", {
|
|
1117
|
-
name:
|
|
1118
|
-
triggers:
|
|
1119
|
-
type:
|
|
1120
|
-
contractId:
|
|
1121
|
-
functionName:
|
|
1122
|
-
caller:
|
|
1123
|
-
sender:
|
|
1124
|
-
recipient:
|
|
1125
|
-
assetIdentifier:
|
|
1126
|
-
deployer:
|
|
1127
|
-
contractName:
|
|
1128
|
-
topic:
|
|
1129
|
-
lockedAddress:
|
|
1130
|
-
trait:
|
|
1131
|
-
minAmount:
|
|
1132
|
-
maxAmount:
|
|
1169
|
+
name: z11.string().describe("Human-readable name, unique per account"),
|
|
1170
|
+
triggers: z11.array(z11.object({
|
|
1171
|
+
type: z11.enum(CHAIN_TRIGGER_TYPES),
|
|
1172
|
+
contractId: z11.string().optional(),
|
|
1173
|
+
functionName: z11.string().optional(),
|
|
1174
|
+
caller: z11.string().optional(),
|
|
1175
|
+
sender: z11.string().optional(),
|
|
1176
|
+
recipient: z11.string().optional(),
|
|
1177
|
+
assetIdentifier: z11.string().optional(),
|
|
1178
|
+
deployer: z11.string().optional(),
|
|
1179
|
+
contractName: z11.string().optional(),
|
|
1180
|
+
topic: z11.string().optional(),
|
|
1181
|
+
lockedAddress: z11.string().optional(),
|
|
1182
|
+
trait: z11.string().optional(),
|
|
1183
|
+
minAmount: z11.union([z11.string(), z11.number()]).optional(),
|
|
1184
|
+
maxAmount: z11.union([z11.string(), z11.number()]).optional()
|
|
1133
1185
|
})).describe("Chain triggers. Each targets a raw chain event/tx; string fields accept `*` wildcards, `trait` scopes to a SIP/trait. Per-type accepted fields: see the secondlayer://chain-triggers resource. Forward-looking: starts at chain tip, no backfill."),
|
|
1134
|
-
url:
|
|
1135
|
-
format:
|
|
1186
|
+
url: z11.string().describe("Webhook URL"),
|
|
1187
|
+
format: z11.enum([
|
|
1136
1188
|
"standard-webhooks",
|
|
1137
1189
|
"inngest",
|
|
1138
1190
|
"trigger",
|
|
@@ -1140,17 +1192,17 @@ function registerWebhookTools(server, clientProvider = getClient) {
|
|
|
1140
1192
|
"cloudevents",
|
|
1141
1193
|
"raw"
|
|
1142
1194
|
]).optional().describe("Wire format (default standard-webhooks)"),
|
|
1143
|
-
authConfig:
|
|
1195
|
+
authConfig: z11.record(z11.string(), z11.unknown()).optional().describe('Receiver auth sent with each delivery, e.g. {"type": "bearer", "token": "..."}')
|
|
1144
1196
|
}, async (input) => {
|
|
1145
1197
|
const res = await clientProvider().webhooks.create(input);
|
|
1146
1198
|
return jsonResponse(res);
|
|
1147
1199
|
});
|
|
1148
1200
|
defineTool(server, "webhooks_update", "Patch a webhook (name, url, authConfig, format, retry, timeout, concurrency).", {
|
|
1149
|
-
id:
|
|
1150
|
-
name:
|
|
1151
|
-
url:
|
|
1152
|
-
authConfig:
|
|
1153
|
-
format:
|
|
1201
|
+
id: webhookIdSchema,
|
|
1202
|
+
name: z11.string().optional().describe("Rename the webhook"),
|
|
1203
|
+
url: z11.string().optional(),
|
|
1204
|
+
authConfig: z11.record(z11.string(), z11.unknown()).optional().describe("Receiver auth sent with each delivery (bearer/etc.)"),
|
|
1205
|
+
format: z11.enum([
|
|
1154
1206
|
"standard-webhooks",
|
|
1155
1207
|
"inngest",
|
|
1156
1208
|
"trigger",
|
|
@@ -1158,35 +1210,35 @@ function registerWebhookTools(server, clientProvider = getClient) {
|
|
|
1158
1210
|
"cloudevents",
|
|
1159
1211
|
"raw"
|
|
1160
1212
|
]).optional(),
|
|
1161
|
-
maxRetries:
|
|
1162
|
-
timeoutMs:
|
|
1163
|
-
concurrency:
|
|
1213
|
+
maxRetries: z11.number().int().min(0).optional(),
|
|
1214
|
+
timeoutMs: z11.number().int().min(100).optional(),
|
|
1215
|
+
concurrency: z11.number().int().min(1).optional()
|
|
1164
1216
|
}, async ({ id, ...patch }) => {
|
|
1165
1217
|
const res = await clientProvider().webhooks.update(id, patch);
|
|
1166
1218
|
return jsonResponse(res);
|
|
1167
1219
|
});
|
|
1168
|
-
defineTool(server, "webhooks_delete", "Delete a webhook. Pending outbox rows are cascade-deleted.", { id:
|
|
1220
|
+
defineTool(server, "webhooks_delete", "Delete a webhook. Pending outbox rows are cascade-deleted.", { id: webhookIdSchema }, async ({ id }) => {
|
|
1169
1221
|
const res = await clientProvider().webhooks.delete(id);
|
|
1170
1222
|
return jsonResponse(res);
|
|
1171
1223
|
});
|
|
1172
|
-
defineTool(server, "webhooks_test", "Send a one-off test webhook to a webhook's URL (built for its format, SSRF-guarded). Logged as a delivery row. Returns {ok, statusCode, error, durationMs, deliveryId}.", { id:
|
|
1224
|
+
defineTool(server, "webhooks_test", "Send a one-off test webhook to a webhook's URL (built for its format, SSRF-guarded). Logged as a delivery row. Returns {ok, statusCode, error, durationMs, deliveryId}.", { id: webhookIdSchema }, async ({ id }) => {
|
|
1173
1225
|
const res = await clientProvider().webhooks.test(id);
|
|
1174
1226
|
return jsonResponse(res);
|
|
1175
1227
|
});
|
|
1176
|
-
defineTool(server, "webhooks_pause", "Pause a webhook: stops delivery attempts and stops queueing new ones. Nothing is deleted — resume picks the webhook back up from the tip. Verify with webhooks_get (status).", { id:
|
|
1177
|
-
defineTool(server, "webhooks_resume", "Resume a paused webhook (also clears a tripped circuit breaker). Deliveries restart from the current tip — use webhooks_replay for the blocks missed while paused. Verify with webhooks_get (status).", { id:
|
|
1178
|
-
defineTool(server, "webhooks_rotate_secret", "Rotate a webhook's signing secret. Returns the NEW `signingSecret` ONCE — forward it to the user; deliveries signed with the old secret stop verifying as soon as this returns, so the receiver must be updated.", { id:
|
|
1179
|
-
defineTool(server, "webhooks_deliveries", "List recent delivery attempts for a webhook — status code, attempt count, error, duration, timestamps. THIS IS THE VERIFY CALL: after webhooks_create, webhooks_test, or webhooks_replay, read it to confirm the webhook actually landed (and to see the receiver's response when it didn't) instead of trusting the enqueue result.", { id:
|
|
1180
|
-
defineTool(server, "webhooks_dead", "List a webhook's dead-letter queue — deliveries that exhausted their retries, with the outbox id, payload, and last error. Diagnose the receiver first (webhooks_get for the URL/auth, webhooks_test for a live probe), then requeue rows with webhooks_requeue.", { id:
|
|
1228
|
+
defineTool(server, "webhooks_pause", "Pause a webhook: stops delivery attempts and stops queueing new ones. Nothing is deleted — resume picks the webhook back up from the tip. Verify with webhooks_get (status).", { id: webhookIdSchema }, async ({ id }) => jsonResponse(await clientProvider().webhooks.pause(id)));
|
|
1229
|
+
defineTool(server, "webhooks_resume", "Resume a paused webhook (also clears a tripped circuit breaker). Deliveries restart from the current tip — use webhooks_replay for the blocks missed while paused. Verify with webhooks_get (status).", { id: webhookIdSchema }, async ({ id }) => jsonResponse(await clientProvider().webhooks.resume(id)));
|
|
1230
|
+
defineTool(server, "webhooks_rotate_secret", "Rotate a webhook's signing secret. Returns the NEW `signingSecret` ONCE — forward it to the user; deliveries signed with the old secret stop verifying as soon as this returns, so the receiver must be updated.", { id: webhookIdSchema }, async ({ id }) => jsonResponse(await clientProvider().webhooks.rotateSecret(id)));
|
|
1231
|
+
defineTool(server, "webhooks_deliveries", "List recent delivery attempts for a webhook — status code, attempt count, error, duration, timestamps. THIS IS THE VERIFY CALL: after webhooks_create, webhooks_test, or webhooks_replay, read it to confirm the webhook actually landed (and to see the receiver's response when it didn't) instead of trusting the enqueue result.", { id: webhookIdSchema }, async ({ id }) => jsonResponse(await clientProvider().webhooks.deliveries(id)));
|
|
1232
|
+
defineTool(server, "webhooks_dead", "List a webhook's dead-letter queue — deliveries that exhausted their retries, with the outbox id, payload, and last error. Diagnose the receiver first (webhooks_get for the URL/auth, webhooks_test for a live probe), then requeue rows with webhooks_requeue.", { id: webhookIdSchema }, async ({ id }) => jsonResponse(await clientProvider().webhooks.dead(id)));
|
|
1181
1233
|
defineTool(server, "webhooks_requeue", "Requeue ONE dead-lettered delivery for another attempt, by its outbox id from webhooks_dead. Fix the receiver first — a requeue against a still-broken endpoint just dies again. Verify with webhooks_deliveries.", {
|
|
1182
|
-
id:
|
|
1183
|
-
outboxId:
|
|
1234
|
+
id: webhookIdSchema,
|
|
1235
|
+
outboxId: uuidSchema.describe("Outbox id of the dead delivery (from webhooks_dead)")
|
|
1184
1236
|
}, async ({ id, outboxId }) => jsonResponse(await clientProvider().webhooks.requeue(id, outboxId)));
|
|
1185
1237
|
defineTool(server, "webhooks_replay", "Replay a block range for a webhook. Replays run at 10% of batch capacity — use sparingly. Pass `force` (a short idempotency suffix) to re-run a range that was already replayed.", {
|
|
1186
|
-
id:
|
|
1187
|
-
fromBlock:
|
|
1188
|
-
toBlock:
|
|
1189
|
-
force:
|
|
1238
|
+
id: webhookIdSchema,
|
|
1239
|
+
fromBlock: z11.number().int().nonnegative(),
|
|
1240
|
+
toBlock: z11.number().int().nonnegative(),
|
|
1241
|
+
force: z11.string().optional().describe("Idempotency suffix to force a duplicate replay of the range")
|
|
1190
1242
|
}, async ({ id, fromBlock, toBlock, force }) => {
|
|
1191
1243
|
const res = await clientProvider().webhooks.replay(id, {
|
|
1192
1244
|
fromBlock,
|
|
@@ -1314,7 +1366,8 @@ function createServer(options = {}) {
|
|
|
1314
1366
|
// src/bin-http.ts
|
|
1315
1367
|
var port = Number.parseInt(process.env.SECONDLAYER_MCP_PORT || "3100");
|
|
1316
1368
|
var secret = process.env.SECONDLAYER_MCP_SECRET;
|
|
1317
|
-
var sessions = new
|
|
1369
|
+
var sessions = new SessionRegistry;
|
|
1370
|
+
sessions.startSweeping();
|
|
1318
1371
|
function authenticate(req) {
|
|
1319
1372
|
if (!secret)
|
|
1320
1373
|
return true;
|
|
@@ -1355,32 +1408,45 @@ var httpServer = createHttpServer(async (req, res) => {
|
|
|
1355
1408
|
const transport = new StreamableHTTPServerTransport({
|
|
1356
1409
|
sessionIdGenerator: () => crypto.randomUUID()
|
|
1357
1410
|
});
|
|
1411
|
+
transport.onclose = () => {
|
|
1412
|
+
if (transport.sessionId)
|
|
1413
|
+
sessions.delete(transport.sessionId);
|
|
1414
|
+
};
|
|
1358
1415
|
const server = createServer();
|
|
1359
|
-
|
|
1360
|
-
|
|
1416
|
+
try {
|
|
1417
|
+
await server.connect(transport);
|
|
1418
|
+
await transport.handleRequest(req, res, body);
|
|
1419
|
+
} catch (err) {
|
|
1420
|
+
await transport.close().catch(() => {});
|
|
1421
|
+
if (!res.headersSent) {
|
|
1422
|
+
res.writeHead(500).end(JSON.stringify({ error: "Session initialization failed" }));
|
|
1423
|
+
}
|
|
1424
|
+
console.error("MCP session init failed:", err);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1361
1427
|
if (transport.sessionId) {
|
|
1362
|
-
sessions.
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
sessions.delete(transport.sessionId);
|
|
1366
|
-
};
|
|
1428
|
+
sessions.add(transport.sessionId, transport);
|
|
1429
|
+
} else {
|
|
1430
|
+
await transport.close().catch(() => {});
|
|
1367
1431
|
}
|
|
1368
1432
|
return;
|
|
1369
1433
|
}
|
|
1370
|
-
|
|
1434
|
+
const existing = sessions.get(sessionId);
|
|
1435
|
+
if (!existing) {
|
|
1371
1436
|
res.writeHead(400).end(JSON.stringify({ error: "Invalid or missing session ID" }));
|
|
1372
1437
|
return;
|
|
1373
1438
|
}
|
|
1374
|
-
await
|
|
1439
|
+
await existing.handleRequest(req, res, body);
|
|
1375
1440
|
} else if (req.method === "GET") {
|
|
1376
|
-
|
|
1441
|
+
const existing = sessions.get(sessionId);
|
|
1442
|
+
if (!existing) {
|
|
1377
1443
|
res.writeHead(400).end(JSON.stringify({ error: "Invalid or missing session ID" }));
|
|
1378
1444
|
return;
|
|
1379
1445
|
}
|
|
1380
|
-
await
|
|
1446
|
+
await existing.handleRequest(req, res);
|
|
1381
1447
|
} else if (req.method === "DELETE") {
|
|
1382
|
-
|
|
1383
|
-
|
|
1448
|
+
const transport = sessions.get(sessionId);
|
|
1449
|
+
if (sessionId && transport) {
|
|
1384
1450
|
await transport.handleRequest(req, res);
|
|
1385
1451
|
await transport.close();
|
|
1386
1452
|
sessions.delete(sessionId);
|
|
@@ -1397,5 +1463,5 @@ httpServer.listen(port, () => {
|
|
|
1397
1463
|
console.error("Warning: SECONDLAYER_MCP_SECRET not set, authentication disabled");
|
|
1398
1464
|
});
|
|
1399
1465
|
|
|
1400
|
-
//# debugId=
|
|
1466
|
+
//# debugId=A0F3507D56CDA04E64756E2164756E21
|
|
1401
1467
|
//# sourceMappingURL=bin-http.js.map
|