@secondlayer/mcp 11.0.0 → 11.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,6 +9,19 @@ status, archive verify/bootstrap, and hosted credits/quote. Everything else
9
9
  (single-record lookups, mempool, stacking, proofs, live Streams reads) is
10
10
  available over REST `/v1` + OpenAPI. There is no `consume` tool.
11
11
 
12
+ ## Hosted
13
+
14
+ No install: the hosted API serves the read tools over Streamable HTTP, with a
15
+ browser sign-in.
16
+
17
+ ```bash
18
+ claude mcp add --transport http secondlayer https://api.secondlayer.tools/mcp
19
+ ```
20
+
21
+ Clients without OAuth send `Authorization: Bearer <sk-sl_ key>`. Subgraph
22
+ deploys, webhooks, archive, codegen and instance tools are local only. See
23
+ [/docs/mcp](https://secondlayer.tools/docs/mcp#connect).
24
+
12
25
  ## Install
13
26
 
14
27
  ```bash
package/dist/bin-http.js CHANGED
@@ -23,6 +23,63 @@ function isHostedArchiveUrl(url) {
23
23
  }
24
24
  }
25
25
 
26
+ // src/lib/tool.ts
27
+ var registeredToolNames = new Set;
28
+ var serverToolNames = new WeakMap;
29
+ var serverToolFilters = new WeakMap;
30
+ function getRegisteredToolNames(server) {
31
+ return server ? [...serverToolNames.get(server) ?? []] : [...registeredToolNames];
32
+ }
33
+ function restrictTools(server, allow) {
34
+ serverToolFilters.set(server, allow);
35
+ }
36
+ function envelopeFields(err) {
37
+ if (!err || typeof err !== "object")
38
+ return {};
39
+ const e = err;
40
+ const body = e.body && typeof e.body === "object" ? e.body : {};
41
+ const code = typeof e.code === "string" ? e.code : typeof body.code === "string" ? body.code : undefined;
42
+ const request_id = typeof body.request_id === "string" ? body.request_id : undefined;
43
+ const fb = body.feedback;
44
+ const feedback = fb && typeof fb === "object" && typeof fb.url === "string" ? { url: fb.url } : undefined;
45
+ return {
46
+ ...code ? { code } : {},
47
+ ...request_id ? { request_id } : {},
48
+ ...feedback ? { feedback } : {}
49
+ };
50
+ }
51
+ function defineTool(server, name, description, schema, handler) {
52
+ const wrappedHandler = async (args) => {
53
+ try {
54
+ return await handler(args);
55
+ } catch (err) {
56
+ const message = err instanceof Error ? err.message : String(err);
57
+ const status = err instanceof Error && "status" in err ? err.status : 0;
58
+ const type = status === 401 ? "unauthorized" : status === 404 ? "not_found" : status === 429 ? "rate_limited" : status >= 500 ? "server_error" : "error";
59
+ return {
60
+ content: [
61
+ {
62
+ type: "text",
63
+ text: JSON.stringify({
64
+ error: { type, status, message, ...envelopeFields(err) }
65
+ })
66
+ }
67
+ ],
68
+ isError: true
69
+ };
70
+ }
71
+ };
72
+ const allow = serverToolFilters.get(server);
73
+ if (allow && !allow(name))
74
+ return;
75
+ if (!allow)
76
+ registeredToolNames.add(name);
77
+ const names = serverToolNames.get(server) ?? new Set;
78
+ names.add(name);
79
+ serverToolNames.set(server, names);
80
+ server.tool(name, description, schema, wrappedHandler);
81
+ }
82
+
26
83
  // src/resources.ts
27
84
  import {
28
85
  ResourceTemplate
@@ -83,37 +140,40 @@ function getArchiveOpsClient() {
83
140
  function keyHint(baseUrl = resolveBaseUrl()) {
84
141
  return isHostedApiUrl(baseUrl) ? " — set SECONDLAYER_API_KEY (an sk-sl_ key from secondlayer.tools/account/keys)" : " — set INSTANCE_TOKEN from `sl init` for writes";
85
142
  }
86
- async function apiRequest(method, path, body) {
87
- const baseUrl = resolveBaseUrl();
88
- const apiKey = readApiKey();
89
- const res = await fetch(`${baseUrl}${path}`, {
90
- method,
91
- headers: {
92
- "Content-Type": "application/json",
93
- ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
94
- },
95
- body: body ? JSON.stringify(body) : undefined
96
- });
97
- if (!res.ok) {
98
- const text = await res.text().catch(() => "");
99
- const needsKey = !apiKey && (res.status === 401 || res.status === 403);
100
- let parsed;
101
- try {
102
- parsed = text ? JSON.parse(text) : undefined;
103
- } catch {
104
- parsed = undefined;
105
- }
106
- const code = parsed && typeof parsed === "object" && typeof parsed.code === "string" ? parsed.code : undefined;
107
- throw Object.assign(new Error((text || `HTTP ${res.status}`) + (needsKey ? keyHint(baseUrl) : "")), {
108
- status: res.status,
109
- ...parsed !== undefined ? { body: parsed } : {},
110
- ...code ? { code } : {}
143
+ function createApiRequest(config) {
144
+ const doFetch = config.fetchImpl ?? ((...args) => fetch(...args));
145
+ return async (method, path, body) => {
146
+ const { baseUrl, apiKey } = config;
147
+ const res = await doFetch(`${baseUrl}${path}`, {
148
+ method,
149
+ headers: {
150
+ "Content-Type": "application/json",
151
+ ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
152
+ },
153
+ body: body ? JSON.stringify(body) : undefined
111
154
  });
112
- }
113
- if (res.status === 204)
114
- return;
115
- return res.json();
155
+ if (!res.ok) {
156
+ const text = await res.text().catch(() => "");
157
+ const needsKey = !apiKey && (res.status === 401 || res.status === 403);
158
+ let parsed;
159
+ try {
160
+ parsed = text ? JSON.parse(text) : undefined;
161
+ } catch {
162
+ parsed = undefined;
163
+ }
164
+ const code = parsed && typeof parsed === "object" && typeof parsed.code === "string" ? parsed.code : undefined;
165
+ throw Object.assign(new Error((text || `HTTP ${res.status}`) + (needsKey ? keyHint(baseUrl) : "")), {
166
+ status: res.status,
167
+ ...parsed !== undefined ? { body: parsed } : {},
168
+ ...code ? { code } : {}
169
+ });
170
+ }
171
+ if (res.status === 204)
172
+ return;
173
+ return res.json();
174
+ };
116
175
  }
176
+ var apiRequest = (method, path, body) => createApiRequest({ baseUrl: resolveBaseUrl(), apiKey: readApiKey() })(method, path, body);
117
177
 
118
178
  // src/lib/format.ts
119
179
  function formatSubgraphSummary(s) {
@@ -144,51 +204,6 @@ function textResponse(text, isError) {
144
204
  };
145
205
  }
146
206
 
147
- // src/lib/tool.ts
148
- var registeredToolNames = new Set;
149
- function getRegisteredToolNames() {
150
- return [...registeredToolNames];
151
- }
152
- function envelopeFields(err) {
153
- if (!err || typeof err !== "object")
154
- return {};
155
- const e = err;
156
- const body = e.body && typeof e.body === "object" ? e.body : {};
157
- const code = typeof e.code === "string" ? e.code : typeof body.code === "string" ? body.code : undefined;
158
- const request_id = typeof body.request_id === "string" ? body.request_id : undefined;
159
- const fb = body.feedback;
160
- const feedback = fb && typeof fb === "object" && typeof fb.url === "string" ? { url: fb.url } : undefined;
161
- return {
162
- ...code ? { code } : {},
163
- ...request_id ? { request_id } : {},
164
- ...feedback ? { feedback } : {}
165
- };
166
- }
167
- function defineTool(server, name, description, schema, handler) {
168
- const wrappedHandler = async (args) => {
169
- try {
170
- return await handler(args);
171
- } catch (err) {
172
- const message = err instanceof Error ? err.message : String(err);
173
- const status = err instanceof Error && "status" in err ? err.status : 0;
174
- const type = status === 401 ? "unauthorized" : status === 404 ? "not_found" : status === 429 ? "rate_limited" : status >= 500 ? "server_error" : "error";
175
- return {
176
- content: [
177
- {
178
- type: "text",
179
- text: JSON.stringify({
180
- error: { type, status, message, ...envelopeFields(err) }
181
- })
182
- }
183
- ],
184
- isError: true
185
- };
186
- }
187
- };
188
- registeredToolNames.add(name);
189
- server.tool(name, description, schema, wrappedHandler);
190
- }
191
-
192
207
  // src/resources.ts
193
208
  var TRAIT_BLURBS = {
194
209
  "sip-009": "Non-fungible token (NFT) standard",
@@ -240,9 +255,9 @@ var PRODUCT_ORDER = [
240
255
  "archive",
241
256
  "credits"
242
257
  ];
243
- function buildCapabilities() {
258
+ function buildCapabilities(server) {
244
259
  const byPrefix = new Map;
245
- for (const name of getRegisteredToolNames()) {
260
+ for (const name of getRegisteredToolNames(server)) {
246
261
  const sep = name.indexOf("_");
247
262
  const prefix = sep === -1 ? name : name.slice(0, sep);
248
263
  const tools = byPrefix.get(prefix) ?? [];
@@ -263,7 +278,11 @@ function buildCapabilities() {
263
278
  discoverFirst: "Call index_discover / contracts_find to learn what exists (and which filters each surface accepts) before querying."
264
279
  };
265
280
  }
266
- function readAuthTiers(credential) {
281
+ function readAuthTiers(credential, hosted) {
282
+ if (hosted) {
283
+ const reads = `keyed: every read uses ${credential} and is metered to the account's credits (the first 1M rows each month are free)`;
284
+ return { index: reads, streams: reads };
285
+ }
267
286
  const reads = `keyless while the API is only reachable on loopback; ${credential} is required once it is published past loopback`;
268
287
  return {
269
288
  index: reads,
@@ -274,8 +293,9 @@ function readAuthTiers(credential) {
274
293
  function credentialLabel() {
275
294
  return isHostedApiUrl2(resolveBaseUrl2()) ? "SECONDLAYER_API_KEY (an sk-sl_ account key)" : "INSTANCE_TOKEN";
276
295
  }
296
+ var HOSTED_CREDENTIAL = "the caller's OAuth sign-in (or sk-sl_ account key)";
277
297
  async function buildContext(deps = { clientProvider: getClient }) {
278
- const credential = credentialLabel();
298
+ const credential = deps.hosted ? HOSTED_CREDENTIAL : credentialLabel();
279
299
  const unavailable = `unavailable: set ${credential}`;
280
300
  const orNull = (field) => {
281
301
  if (!field)
@@ -288,7 +308,7 @@ async function buildContext(deps = { clientProvider: getClient }) {
288
308
  const instance = snap?.instance?.value ? snap.instance.value : orNull(snap?.instance);
289
309
  return {
290
310
  authState: {
291
- apiKeySet: Boolean(readApiKey())
311
+ apiKeySet: deps.hosted ? true : Boolean(readApiKey())
292
312
  },
293
313
  whatExists: {
294
314
  account: orNull(snap?.account),
@@ -299,12 +319,13 @@ async function buildContext(deps = { clientProvider: getClient }) {
299
319
  activeOperations: orNull(snap?.activeOperations),
300
320
  instance
301
321
  },
302
- whatYouCanDo: buildCapabilities(),
303
- readAuthTiers: readAuthTiers(credential),
322
+ whatYouCanDo: buildCapabilities(deps.server),
323
+ readAuthTiers: readAuthTiers(credential, Boolean(deps.hosted)),
304
324
  ...snap?.instance?.value?.state === "empty-index" ? { nextStep: BOOTSTRAP_STEP } : {}
305
325
  };
306
326
  }
307
- function registerResources(server) {
327
+ function registerResources(server, deps = { clientProvider: getClient }) {
328
+ const contextDeps = deps.hosted ? { ...deps, server } : deps;
308
329
  server.resource("context", "secondlayer://context", {
309
330
  description: "Live agent context — what exists (your subgraphs, webhooks, account), what you can do, and read-auth tiers. Read this first."
310
331
  }, async () => ({
@@ -312,7 +333,7 @@ function registerResources(server) {
312
333
  {
313
334
  uri: "secondlayer://context",
314
335
  mimeType: "application/json",
315
- text: JSON.stringify(await buildContext(), null, 2)
336
+ text: JSON.stringify(await buildContext(contextDeps), null, 2)
316
337
  }
317
338
  ]
318
339
  }));
@@ -350,24 +371,26 @@ function registerResources(server) {
350
371
  }
351
372
  ]
352
373
  }));
353
- server.resource("chain-triggers", "secondlayer://chain-triggers", {
354
- description: "Chain-webhook trigger types and the filter fields each accepts (for webhooks_create triggers)."
355
- }, async () => ({
356
- contents: [
357
- {
358
- uri: "secondlayer://chain-triggers",
359
- mimeType: "application/json",
360
- text: JSON.stringify(CHAIN_TRIGGER_FIELDS, null, 2)
361
- }
362
- ]
363
- }));
374
+ if (!deps.hosted) {
375
+ server.resource("chain-triggers", "secondlayer://chain-triggers", {
376
+ description: "Chain-webhook trigger types and the filter fields each accepts (for webhooks_create triggers)."
377
+ }, async () => ({
378
+ contents: [
379
+ {
380
+ uri: "secondlayer://chain-triggers",
381
+ mimeType: "application/json",
382
+ text: JSON.stringify(CHAIN_TRIGGER_FIELDS, null, 2)
383
+ }
384
+ ]
385
+ }));
386
+ }
364
387
  server.resource("samples", new ResourceTemplate("secondlayer://samples/{contractId}", {
365
388
  list: undefined
366
389
  }), {
367
390
  description: "Decoded camelCase print samples for a contract (schema + up to 5 events). Use before writing a print_event handler."
368
391
  }, async (uri, variables) => {
369
392
  const contractId = String(Array.isArray(variables.contractId) ? variables.contractId[0] : variables.contractId ?? "");
370
- const body = await buildSamplesResource(contractId);
393
+ const body = await buildSamplesResource(contractId, deps.clientProvider);
371
394
  return {
372
395
  contents: [
373
396
  {
@@ -432,14 +455,14 @@ async function buildSamplesResource(contractId, clientProvider = getClient) {
432
455
 
433
456
  // src/tools/account.ts
434
457
  import { z } from "zod";
435
- function registerAccountTools(server) {
458
+ function registerAccountTools(server, request = apiRequest) {
436
459
  defineTool(server, "account_whoami", "Show the archive account email at api.secondlayer.tools, not the self-host instance.", {}, async () => {
437
- const result = await apiRequest("GET", "/api/accounts/me");
460
+ const result = await request("GET", "/api/accounts/me");
438
461
  return jsonResponse(result);
439
462
  });
440
463
  defineTool(server, "account_create_key", "Mint an account key (sk-sl_*) for archive credits, quote, and fetch at api.secondlayer.tools. The returned `key` is shown ONCE.", {
441
464
  name: z.string().optional().describe("Optional label for the key")
442
- }, async ({ name }) => jsonResponse(await apiRequest("POST", "/api/keys", { name })));
465
+ }, async ({ name }) => jsonResponse(await request("POST", "/api/keys", { name })));
443
466
  }
444
467
 
445
468
  // src/tools/archive-cli.ts
@@ -1144,35 +1167,103 @@ function registerWebhookTools(server, clientProvider = getClient) {
1144
1167
  }
1145
1168
 
1146
1169
  // src/tool-groups.ts
1170
+ var ENV_TOOL_DEPS = {
1171
+ clientProvider: getClient,
1172
+ archiveOpsClientProvider: getArchiveOpsClient,
1173
+ apiRequest
1174
+ };
1147
1175
  var TOOL_GROUPS = [
1148
- { group: "scaffold", register: registerScaffoldTools },
1149
- { group: "subgraphs", register: registerSubgraphTools },
1150
- { group: "webhooks", register: registerWebhookTools },
1151
- { group: "index", register: registerIndexTools },
1152
- { group: "streams", register: registerStreamsTools },
1153
- { group: "contracts", register: registerContractTools },
1154
- { group: "codegen", register: registerCodegenTools },
1155
- { group: "instance", register: registerInstanceTools },
1156
- { group: "archive", register: registerArchiveTools },
1157
- { group: "archive", register: registerArchiveCliTools },
1158
- { group: "account", register: registerAccountTools, hostedOnly: true }
1176
+ {
1177
+ group: "scaffold",
1178
+ register: (s, d = ENV_TOOL_DEPS) => registerScaffoldTools(s, d.clientProvider),
1179
+ hosted: true
1180
+ },
1181
+ {
1182
+ group: "subgraphs",
1183
+ register: (s, d = ENV_TOOL_DEPS) => registerSubgraphTools(s, d.clientProvider),
1184
+ hosted: false
1185
+ },
1186
+ {
1187
+ group: "webhooks",
1188
+ register: (s, d = ENV_TOOL_DEPS) => registerWebhookTools(s, d.clientProvider),
1189
+ hosted: false
1190
+ },
1191
+ {
1192
+ group: "index",
1193
+ register: (s, d = ENV_TOOL_DEPS) => registerIndexTools(s, d.clientProvider),
1194
+ hosted: true
1195
+ },
1196
+ {
1197
+ group: "streams",
1198
+ register: (s, d = ENV_TOOL_DEPS) => registerStreamsTools(s, d.clientProvider),
1199
+ hosted: true
1200
+ },
1201
+ {
1202
+ group: "contracts",
1203
+ register: (s, d = ENV_TOOL_DEPS) => registerContractTools(s, d.clientProvider),
1204
+ hosted: true
1205
+ },
1206
+ { group: "codegen", register: (s) => registerCodegenTools(s), hosted: false },
1207
+ {
1208
+ group: "instance",
1209
+ register: (s, d = ENV_TOOL_DEPS) => registerInstanceTools(s, d.clientProvider),
1210
+ hosted: false
1211
+ },
1212
+ {
1213
+ group: "archive",
1214
+ register: (s, d = ENV_TOOL_DEPS) => registerArchiveTools(s, d.clientProvider, d.archiveOpsClientProvider),
1215
+ hosted: false
1216
+ },
1217
+ {
1218
+ group: "archive",
1219
+ register: (s) => registerArchiveCliTools(s),
1220
+ hosted: false
1221
+ },
1222
+ {
1223
+ group: "account",
1224
+ register: (s, d = ENV_TOOL_DEPS) => registerAccountTools(s, d.apiRequest),
1225
+ hostedOnly: true,
1226
+ hosted: ["account_whoami"]
1227
+ }
1159
1228
  ];
1229
+ function isHostedTool(group, tool) {
1230
+ return group.hosted === true || Array.isArray(group.hosted) && group.hosted.includes(tool);
1231
+ }
1160
1232
 
1161
1233
  // src/server.ts
1162
1234
  var __dirname2 = dirname(fileURLToPath(import.meta.url));
1163
1235
  var pkg = JSON.parse(readFileSync(join2(__dirname2, "../package.json"), "utf-8"));
1164
- function createServer() {
1236
+ function createServer(options = {}) {
1165
1237
  const server = new McpServer({
1166
1238
  name: "secondlayer",
1167
1239
  version: pkg.version
1168
1240
  });
1169
- const hosted = isHostedArchiveUrl(resolveBaseUrl3());
1241
+ const deps = {
1242
+ clientProvider: options.clientProvider ?? ENV_TOOL_DEPS.clientProvider,
1243
+ archiveOpsClientProvider: options.archiveOpsClientProvider ?? ENV_TOOL_DEPS.archiveOpsClientProvider,
1244
+ apiRequest: options.apiRequest ?? ENV_TOOL_DEPS.apiRequest
1245
+ };
1246
+ if (options.hosted) {
1247
+ for (const group of TOOL_GROUPS) {
1248
+ if (group.hosted === false)
1249
+ continue;
1250
+ restrictTools(server, (name) => isHostedTool(group, name));
1251
+ group.register(server, deps);
1252
+ }
1253
+ restrictTools(server, () => false);
1254
+ registerResources(server, {
1255
+ clientProvider: deps.clientProvider,
1256
+ hosted: true
1257
+ });
1258
+ return server;
1259
+ }
1260
+ const hostedApi = isHostedArchiveUrl(resolveBaseUrl3());
1170
1261
  for (const { register, hostedOnly } of TOOL_GROUPS) {
1171
- if (hostedOnly && !hosted)
1262
+ if (hostedOnly && !hostedApi)
1172
1263
  continue;
1173
- register(server);
1264
+ register(server, deps);
1174
1265
  }
1175
- registerResources(server);
1266
+ registerResources(server, { clientProvider: deps.clientProvider });
1176
1267
  return server;
1177
1268
  }
1178
1269
 
@@ -1262,5 +1353,5 @@ httpServer.listen(port, () => {
1262
1353
  console.error("Warning: SECONDLAYER_MCP_SECRET not set, authentication disabled");
1263
1354
  });
1264
1355
 
1265
- //# debugId=52D7E49B3C2D102864756E2164756E21
1356
+ //# debugId=64D4C1ABC06CBDC864756E2164756E21
1266
1357
  //# sourceMappingURL=bin-http.js.map