@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 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 accountKey = readArchiveApiKey();
137
- if (!accountKey) {
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: z9.string().describe("Subgraph name") }, async ({ 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: z9.string().describe("Subgraph name"),
911
- format: z9.enum(["agent", "openapi", "markdown"]).optional().describe("agent = JSON schema for tool use (default), openapi = OpenAPI JSON, markdown = docs.md")
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: z9.string().describe("Subgraph name"),
922
- table: z9.string().describe("Table name"),
923
- filters: z9.record(z9.string(), z9.string()).optional().describe('Column filters — plain values or with operators (e.g. {"amount.gte": "1000", "sender": "SP..."})'),
924
- sort: z9.string().optional().describe("Column to sort by"),
925
- order: z9.enum(["asc", "desc"]).optional().describe("Sort order"),
926
- limit: z9.number().max(200).optional().describe("Max rows (default 50, max 200)"),
927
- cursor: z9.string().optional().describe("Resume from a previous response's nextCursor"),
928
- fields: z9.string().optional().describe('Comma-separated column list to return (e.g. "sender,amount")'),
929
- count: z9.boolean().optional().describe("If true, return row count instead of rows")
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: z9.string().describe("Subgraph 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: z9.string().describe("Subgraph name"),
966
- fromBlock: z9.number().int().nonnegative().describe("Start block (inclusive)"),
967
- toBlock: z9.number().int().nonnegative().describe("End block (inclusive)")
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: z9.string().describe("Subgraph name") }, async ({ 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: z9.string().describe("Subgraph name"),
981
- limit: z9.number().int().positive().optional().describe("Max gaps to return"),
982
- offset: z9.number().int().nonnegative().optional().describe("Pagination offset"),
983
- resolved: z9.boolean().optional().describe("Filter by resolved state (omit for unresolved only)")
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: z9.string().describe("Subgraph name"),
994
- operationId: z9.string().optional().describe("Fetch one operation by id (the operationId returned by reindex/backfill/stop); omit for recent history")
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: z9.string().describe("Subgraph name") }, async ({ 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: z9.string().describe("TypeScript source code containing a defineSubgraph() call"),
1005
- startBlock: z9.number().int().nonnegative().optional().describe("Override the definition startBlock for this deploy"),
1006
- dryRun: z9.boolean().optional().describe("Validate schema/DDL only — does not run handlers. Use subgraphs_test to prove a mapping writes rows.")
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: z9.string().describe("TypeScript source containing a defineSubgraph() call"),
1023
- fromHeight: z9.number().int().nonnegative().describe("Start block height (inclusive)"),
1024
- toHeight: z9.number().int().nonnegative().optional().describe("End block height (inclusive); default fromHeight+100"),
1025
- limit: z9.number().int().positive().max(200).optional().describe("Max Index rows per source (default 50, max 200)")
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 z10 } from "zod";
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: z10.string().describe("Webhook id") }, async ({ 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: z10.string().describe("Human-readable name, unique per account"),
1118
- triggers: z10.array(z10.object({
1119
- type: z10.enum(CHAIN_TRIGGER_TYPES),
1120
- contractId: z10.string().optional(),
1121
- functionName: z10.string().optional(),
1122
- caller: z10.string().optional(),
1123
- sender: z10.string().optional(),
1124
- recipient: z10.string().optional(),
1125
- assetIdentifier: z10.string().optional(),
1126
- deployer: z10.string().optional(),
1127
- contractName: z10.string().optional(),
1128
- topic: z10.string().optional(),
1129
- lockedAddress: z10.string().optional(),
1130
- trait: z10.string().optional(),
1131
- minAmount: z10.union([z10.string(), z10.number()]).optional(),
1132
- maxAmount: z10.union([z10.string(), z10.number()]).optional()
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: z10.string().describe("Webhook URL"),
1135
- format: z10.enum([
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: z10.record(z10.string(), z10.unknown()).optional().describe('Receiver auth sent with each delivery, e.g. {"type": "bearer", "token": "..."}')
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: z10.string(),
1150
- name: z10.string().optional().describe("Rename the webhook"),
1151
- url: z10.string().optional(),
1152
- authConfig: z10.record(z10.string(), z10.unknown()).optional().describe("Receiver auth sent with each delivery (bearer/etc.)"),
1153
- format: z10.enum([
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: z10.number().int().min(0).optional(),
1162
- timeoutMs: z10.number().int().min(100).optional(),
1163
- concurrency: z10.number().int().min(1).optional()
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: z10.string() }, async ({ 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: z10.string().describe("Webhook id") }, async ({ 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: z10.string().describe("Webhook id") }, async ({ id }) => jsonResponse(await clientProvider().webhooks.pause(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: z10.string().describe("Webhook id") }, async ({ id }) => jsonResponse(await clientProvider().webhooks.resume(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: z10.string().describe("Webhook id") }, async ({ id }) => jsonResponse(await clientProvider().webhooks.rotateSecret(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: z10.string().describe("Webhook id") }, async ({ id }) => jsonResponse(await clientProvider().webhooks.deliveries(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: z10.string().describe("Webhook id") }, async ({ id }) => jsonResponse(await clientProvider().webhooks.dead(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: z10.string().describe("Webhook id"),
1183
- outboxId: z10.string().describe("Outbox id of the dead delivery (from webhooks_dead)")
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: z10.string(),
1187
- fromBlock: z10.number().int().nonnegative(),
1188
- toBlock: z10.number().int().nonnegative(),
1189
- force: z10.string().optional().describe("Idempotency suffix to force a duplicate replay of the range")
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 Map;
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
- await server.connect(transport);
1360
- await transport.handleRequest(req, res, body);
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.set(transport.sessionId, transport);
1363
- transport.onclose = () => {
1364
- if (transport.sessionId)
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
- if (!sessionId || !sessions.has(sessionId)) {
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 sessions.get(sessionId)?.handleRequest(req, res, body);
1439
+ await existing.handleRequest(req, res, body);
1375
1440
  } else if (req.method === "GET") {
1376
- if (!sessionId || !sessions.has(sessionId)) {
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 sessions.get(sessionId)?.handleRequest(req, res);
1446
+ await existing.handleRequest(req, res);
1381
1447
  } else if (req.method === "DELETE") {
1382
- if (sessionId && sessions.has(sessionId)) {
1383
- const transport = sessions.get(sessionId);
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=653BD5FE7E7CAFBD64756E2164756E21
1466
+ //# debugId=A0F3507D56CDA04E64756E2164756E21
1401
1467
  //# sourceMappingURL=bin-http.js.map