@sirrlock/mcp 0.1.0 → 1.0.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/index.js CHANGED
@@ -6,8 +6,8 @@
6
6
  * Exposes Sirr as MCP tools so Claude Code can read/write ephemeral secrets.
7
7
  *
8
8
  * Configuration (env vars):
9
- * SIRR_SERVER — Sirr server URL (default: http://localhost:39999)
10
- * SIRR_TOKEN — Bearer token (SIRR_MASTER_KEY on the server)
9
+ * SIRR_SERVER — Sirr server URL (default: https://sirr.sirrlock.com)
10
+ * SIRR_TOKEN — Bearer token: SIRR_MASTER_KEY for full access, or a principal key for org-scoped access
11
11
  *
12
12
  * Install: npm install -g @sirrlock/mcp
13
13
  * Configure in .mcp.json:
@@ -26,8 +26,9 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
26
26
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
27
27
  const package_json_1 = require("../package.json");
28
28
  // ── Config ────────────────────────────────────────────────────────────────────
29
- const SIRR_SERVER = (process.env["SIRR_SERVER"] ?? "http://localhost:39999").replace(/\/$/, "");
29
+ const SIRR_SERVER = (process.env["SIRR_SERVER"] ?? "https://sirr.sirrlock.com").replace(/\/$/, "");
30
30
  const SIRR_TOKEN = process.env["SIRR_TOKEN"] ?? "";
31
+ const SIRRLOCK_URL = (process.env["SIRRLOCK_URL"] ?? "https://sirrlock.com").replace(/\/$/, "");
31
32
  // ── Fetch with timeout ────────────────────────────────────────────────────────
32
33
  async function fetchWithTimeout(url, options = {}, ms = 10000) {
33
34
  const controller = new AbortController();
@@ -54,8 +55,9 @@ async function fetchWithTimeout(url, options = {}, ms = 10000) {
54
55
  const STATUS_HINTS = {
55
56
  401: "Check that SIRR_TOKEN matches SIRR_MASTER_KEY on the server. See sirr.dev/errors#401",
56
57
  402: "Free tier limit reached. See sirr.dev/errors#402",
57
- 403: "This API key does not have permission for this operation. See sirr.dev/errors#403",
58
- 404: "Secret not found, expired, or burned. See sirr.dev/errors#404",
58
+ 403: "This token does not have permission for this operation. See sirr.dev/errors#403",
59
+ 404: "Resource not found, expired, or burned. See sirr.dev/errors#404",
60
+ 409: "Conflict — a secret with that key already exists in this org. Use patch_secret to update it. See sirr.dev/errors#409",
59
61
  500: "Server-side error. See sirr.dev/errors#500",
60
62
  };
61
63
  function throwSirrError(status, json) {
@@ -72,49 +74,79 @@ async function sirrRequest(method, path, body) {
72
74
  },
73
75
  body: body !== undefined ? JSON.stringify(body) : undefined,
74
76
  });
75
- const json = (await res.json());
76
77
  if (!res.ok) {
78
+ let json = {};
79
+ try {
80
+ json = (await res.json());
81
+ }
82
+ catch {
83
+ json = { error: await res.text().catch(() => "unknown") };
84
+ }
77
85
  throwSirrError(res.status, json);
78
86
  }
79
- return json;
87
+ return (await res.json());
80
88
  }
81
89
  const helpers_1 = require("./helpers");
82
90
  // ── Tool definitions ──────────────────────────────────────────────────────────
83
91
  const TOOLS = [
84
92
  {
85
93
  name: "get_secret",
86
- description: "Retrieve a secret from the Sirr vault by key name. " +
87
- "The secret's read counter is incrementedif it was set with max_reads=1 it will be deleted after this call. " +
94
+ description: "Retrieve a secret from Sirr. " +
95
+ "Two modes: (1) Public dead dropprovide 'id' (hex64 returned by push_secret). " +
96
+ "Fetches from GET /secrets/{id}. The read counter is incremented — if max_reads=1 the secret burns after this call. " +
97
+ "(2) Org-scoped — provide 'key' and 'org' to fetch a named secret from an organization's vault. " +
88
98
  "Returns null if the secret does not exist, has expired, or has been burned. " +
89
- "Accepts bare key names, 'sirr:KEYNAME' references, or 'KEYNAME#id' format. " +
90
99
  "IMPORTANT: Do not store, log, memorize, or repeat the returned secret value beyond its immediate use. " +
91
100
  "Treat it as ephemeral — use it once for its intended purpose and discard it.",
92
101
  inputSchema: {
93
102
  type: "object",
94
103
  properties: {
104
+ id: {
105
+ type: "string",
106
+ description: "Public secret ID (hex64) returned by push_secret. Use for public dead-drop retrieval.",
107
+ },
95
108
  key: {
96
109
  type: "string",
97
- description: "Secret key name. Accepts 'sirr:KEYNAME', 'KEYNAME#id', or bare key name.",
110
+ description: "Secret key name for org-scoped retrieval. Accepts 'sirr:KEYNAME', 'KEYNAME#id', or bare key name.",
111
+ },
112
+ org: {
113
+ type: "string",
114
+ description: "Organization ID for org-scoped retrieval. Required when using 'key'.",
98
115
  },
99
116
  },
100
- required: ["key"],
101
117
  },
102
118
  },
103
119
  {
104
- name: "push_secret",
105
- description: "Store a secret in the Sirr vault. Optionally set a TTL (seconds) and/or a max read limit. " +
106
- "Use max_reads=1 for one-time credentials that burn after first access. " +
107
- "Use ttl_seconds for time-expiring secrets.",
120
+ name: "check_secret",
121
+ description: "Check whether a secret exists and inspect its metadata WITHOUT consuming a read. " +
122
+ "Use this to verify a secret is still available before fetching it, or to inspect read counts and expiry. " +
123
+ "Returns status (active/sealed), reads used/remaining, and expiry. " +
124
+ "A 'sealed' secret has exhausted its max_reads; it still exists but cannot be read.",
108
125
  inputSchema: {
109
126
  type: "object",
110
127
  properties: {
111
128
  key: {
112
129
  type: "string",
113
- description: "Key name to store the secret under.",
130
+ description: "Secret key name. Accepts 'sirr:KEYNAME', 'KEYNAME#id', or bare key name.",
114
131
  },
132
+ },
133
+ required: ["key"],
134
+ },
135
+ },
136
+ {
137
+ name: "push_secret",
138
+ description: "Push a secret as an anonymous public dead drop — no key, no org required. " +
139
+ "Returns an opaque ID and a one-time URL. Share the URL; the recipient opens it once and it burns. " +
140
+ "Optionally set a TTL (seconds) and/or max read limit. " +
141
+ "Use max_reads=1 (default) for one-time credentials that burn after first access. " +
142
+ "Use ttl_seconds to add a time-based expiry. " +
143
+ "IMPORTANT: Do not store, log, or repeat the pushed value after the call.",
144
+ inputSchema: {
145
+ type: "object",
146
+ properties: {
115
147
  value: {
116
148
  type: "string",
117
- description: "Secret value.",
149
+ description: "Secret value to push.",
118
150
  },
119
151
  ttl_seconds: {
120
152
  type: "number",
@@ -122,10 +154,35 @@ const TOOLS = [
122
154
  },
123
155
  max_reads: {
124
156
  type: "number",
125
- description: "Optional maximum read count. Set to 1 for a one-time secret.",
157
+ description: "Optional maximum read count. Default: 1 (burn after first read).",
126
158
  },
127
159
  },
128
- required: ["key", "value"],
160
+ required: ["value"],
161
+ },
162
+ },
163
+ {
164
+ name: "set_secret",
165
+ description: "Store a named secret in an organization's vault (org-scoped). " +
166
+ "Requires an org ID and a key name. Returns the key and its internal ID. " +
167
+ "Returns a 409 Conflict error if a secret with that key already exists — use patch_secret to update it. " +
168
+ "Use push_secret instead if you want an anonymous, keyless dead drop.",
169
+ inputSchema: {
170
+ type: "object",
171
+ properties: {
172
+ org: {
173
+ type: "string",
174
+ description: "Organization ID to store the secret in.",
175
+ },
176
+ key: {
177
+ type: "string",
178
+ description: "Key name to store the secret under.",
179
+ },
180
+ value: {
181
+ type: "string",
182
+ description: "Secret value.",
183
+ },
184
+ },
185
+ required: ["org", "key", "value"],
129
186
  },
130
187
  },
131
188
  {
@@ -151,6 +208,32 @@ const TOOLS = [
151
208
  required: ["key"],
152
209
  },
153
210
  },
211
+ {
212
+ name: "patch_secret",
213
+ description: "Update an existing secret's value, TTL, or max read count. All fields are optional — only provided fields are changed.",
214
+ inputSchema: {
215
+ type: "object",
216
+ properties: {
217
+ key: {
218
+ type: "string",
219
+ description: "Key name to update.",
220
+ },
221
+ value: {
222
+ type: "string",
223
+ description: "New secret value.",
224
+ },
225
+ ttl_seconds: {
226
+ type: "number",
227
+ description: "New TTL in seconds from now.",
228
+ },
229
+ max_reads: {
230
+ type: "number",
231
+ description: "New max read count.",
232
+ },
233
+ },
234
+ required: ["key"],
235
+ },
236
+ },
154
237
  {
155
238
  name: "prune_secrets",
156
239
  description: "Trigger an immediate sweep of all expired secrets on the server. " +
@@ -168,6 +251,24 @@ const TOOLS = [
168
251
  properties: {},
169
252
  },
170
253
  },
254
+ {
255
+ name: "share_secret",
256
+ description: "Share a sensitive value via a secure burn-after-read link hosted on sirrlock.com. " +
257
+ "No account or token required. " +
258
+ "The link expires after 24 hours or after the recipient opens it once — whichever comes first. " +
259
+ "Returns a URL to send to the recipient. " +
260
+ "IMPORTANT: Do not store or repeat the secret value. Use the returned URL only.",
261
+ inputSchema: {
262
+ type: "object",
263
+ properties: {
264
+ value: {
265
+ type: "string",
266
+ description: "The sensitive value to share (password, token, link, etc.).",
267
+ },
268
+ },
269
+ required: ["value"],
270
+ },
271
+ },
171
272
  {
172
273
  name: "sirr_audit",
173
274
  description: "Query the Sirr audit log. Returns recent events like secret creates, reads, deletes. " +
@@ -179,13 +280,17 @@ const TOOLS = [
179
280
  type: "number",
180
281
  description: "Only return events after this Unix timestamp.",
181
282
  },
283
+ until: {
284
+ type: "number",
285
+ description: "Only return events before this Unix timestamp.",
286
+ },
182
287
  action: {
183
288
  type: "string",
184
289
  description: "Filter by action type (e.g. secret.create, secret.read, key.create).",
185
290
  },
186
291
  limit: {
187
292
  type: "number",
188
- description: "Maximum events to return (default: 50).",
293
+ description: "Maximum events to return (default: 100, max: 1000).",
189
294
  },
190
295
  },
191
296
  },
@@ -233,50 +338,188 @@ const TOOLS = [
233
338
  },
234
339
  },
235
340
  {
236
- name: "sirr_key_create",
237
- description: "Create a scoped API key with specific permissions. " +
238
- "The raw key is returned once — save it immediately. " +
239
- "Permissions: read, write, delete, admin. Optional prefix scoping limits access to secrets matching the prefix.",
341
+ name: "sirr_key_list",
342
+ description: "List all API keys for the current principal. Key values are never returned.",
343
+ inputSchema: {
344
+ type: "object",
345
+ properties: {},
346
+ },
347
+ },
348
+ {
349
+ name: "sirr_me",
350
+ description: "Get the current authenticated user/org profile from the Sirr server. " +
351
+ "Returns account details and current plan information.",
352
+ inputSchema: {
353
+ type: "object",
354
+ properties: {},
355
+ },
356
+ },
357
+ {
358
+ name: "sirr_update_me",
359
+ description: "Update the current principal's metadata on the Sirr server.",
360
+ inputSchema: {
361
+ type: "object",
362
+ properties: {
363
+ metadata: {
364
+ type: "object",
365
+ additionalProperties: { type: "string" },
366
+ description: "Key/value metadata to set on the principal (replaces existing metadata).",
367
+ },
368
+ },
369
+ required: ["metadata"],
370
+ },
371
+ },
372
+ {
373
+ name: "sirr_create_key",
374
+ description: "Create a new API key for the current principal via /me/keys. " +
375
+ "The raw key is returned once — save it immediately.",
240
376
  inputSchema: {
241
377
  type: "object",
242
378
  properties: {
243
- label: {
379
+ name: {
244
380
  type: "string",
245
- description: "Human-readable label for the key.",
381
+ description: "Human-readable name for the key.",
246
382
  },
247
- permissions: {
248
- type: "array",
249
- items: { type: "string" },
250
- description: "Permissions to grant: read, write, delete, admin.",
383
+ valid_for_seconds: {
384
+ type: "number",
385
+ description: "How long the key is valid, in seconds (default: 1 year).",
386
+ },
387
+ valid_before: {
388
+ type: "number",
389
+ description: "Explicit expiry as a Unix timestamp (alternative to valid_for_seconds).",
251
390
  },
252
- prefix: {
391
+ },
392
+ required: ["name"],
393
+ },
394
+ },
395
+ {
396
+ name: "sirr_delete_key",
397
+ description: "Revoke an API key belonging to the current principal.",
398
+ inputSchema: {
399
+ type: "object",
400
+ properties: {
401
+ keyId: {
253
402
  type: "string",
254
- description: "Optional prefix scope — key can only access secrets starting with this prefix.",
403
+ description: "API key ID to delete.",
255
404
  },
256
405
  },
257
- required: ["label", "permissions"],
406
+ required: ["keyId"],
258
407
  },
259
408
  },
409
+ // ── Org management ──────────────────────────────────────────────────────────
260
410
  {
261
- name: "sirr_key_list",
262
- description: "List all scoped API keys. Key hashes are never returned.",
411
+ name: "sirr_org_create",
412
+ description: "Create a new organization. Requires master key or sirr_admin permission.",
263
413
  inputSchema: {
264
414
  type: "object",
265
- properties: {},
415
+ properties: {
416
+ name: { type: "string", description: "Organization name (1–128 chars)." },
417
+ metadata: {
418
+ type: "object",
419
+ additionalProperties: { type: "string" },
420
+ description: "Optional key/value metadata.",
421
+ },
422
+ },
423
+ required: ["name"],
266
424
  },
267
425
  },
268
426
  {
269
- name: "sirr_key_delete",
270
- description: "Delete a scoped API key by its ID.",
427
+ name: "sirr_org_list",
428
+ description: "List all organizations. Requires master key.",
429
+ inputSchema: { type: "object", properties: {} },
430
+ },
431
+ {
432
+ name: "sirr_org_delete",
433
+ description: "Delete an organization by ID. Org must have no principals. Requires master key.",
271
434
  inputSchema: {
272
435
  type: "object",
273
436
  properties: {
274
- id: {
437
+ org_id: { type: "string", description: "Organization ID to delete." },
438
+ },
439
+ required: ["org_id"],
440
+ },
441
+ },
442
+ // ── Principal management ─────────────────────────────────────────────────────
443
+ {
444
+ name: "sirr_principal_create",
445
+ description: "Create a principal (user/service) in an organization.",
446
+ inputSchema: {
447
+ type: "object",
448
+ properties: {
449
+ org_id: { type: "string", description: "Organization ID." },
450
+ name: { type: "string", description: "Principal name (1–128 chars)." },
451
+ role: { type: "string", description: "Role name (must exist in the org or be a built-in role)." },
452
+ metadata: {
453
+ type: "object",
454
+ additionalProperties: { type: "string" },
455
+ description: "Optional key/value metadata.",
456
+ },
457
+ },
458
+ required: ["org_id", "name", "role"],
459
+ },
460
+ },
461
+ {
462
+ name: "sirr_principal_list",
463
+ description: "List all principals in an organization.",
464
+ inputSchema: {
465
+ type: "object",
466
+ properties: {
467
+ org_id: { type: "string", description: "Organization ID." },
468
+ },
469
+ required: ["org_id"],
470
+ },
471
+ },
472
+ {
473
+ name: "sirr_principal_delete",
474
+ description: "Delete a principal from an organization. Principal must have no active keys.",
475
+ inputSchema: {
476
+ type: "object",
477
+ properties: {
478
+ org_id: { type: "string", description: "Organization ID." },
479
+ principal_id: { type: "string", description: "Principal ID to delete." },
480
+ },
481
+ required: ["org_id", "principal_id"],
482
+ },
483
+ },
484
+ // ── Role management ──────────────────────────────────────────────────────────
485
+ {
486
+ name: "sirr_role_create",
487
+ description: "Create a custom role in an organization. " +
488
+ "Permissions are a letter string: C=create, R=read, P=patch, D=delete, L=list, M=manage, A=admin.",
489
+ inputSchema: {
490
+ type: "object",
491
+ properties: {
492
+ org_id: { type: "string", description: "Organization ID." },
493
+ name: { type: "string", description: "Role name (1–64 chars)." },
494
+ permissions: {
275
495
  type: "string",
276
- description: "API key ID to delete.",
496
+ description: "Permission letters, e.g. 'CRL' for create+read+list.",
277
497
  },
278
498
  },
279
- required: ["id"],
499
+ required: ["org_id", "name", "permissions"],
500
+ },
501
+ },
502
+ {
503
+ name: "sirr_role_list",
504
+ description: "List all roles in an organization (built-in and custom).",
505
+ inputSchema: {
506
+ type: "object",
507
+ properties: {
508
+ org_id: { type: "string", description: "Organization ID." },
509
+ },
510
+ required: ["org_id"],
511
+ },
512
+ },
513
+ {
514
+ name: "sirr_role_delete",
515
+ description: "Delete a custom role from an organization. Cannot delete built-in roles or roles in use.",
516
+ inputSchema: {
517
+ type: "object",
518
+ properties: {
519
+ org_id: { type: "string", description: "Organization ID." },
520
+ role_name: { type: "string", description: "Role name to delete." },
521
+ },
522
+ required: ["org_id", "role_name"],
280
523
  },
281
524
  },
282
525
  ];
@@ -290,21 +533,51 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
290
533
  try {
291
534
  switch (name) {
292
535
  case "get_secret": {
293
- const rawKey = args.key;
294
- const key = (0, helpers_1.parseKeyRef)(rawKey);
295
- const res = await fetchWithTimeout(`${SIRR_SERVER}/secrets/${encodeURIComponent(key)}`, { headers: { Authorization: `Bearer ${SIRR_TOKEN}` } });
296
- if (res.status === 404) {
536
+ const { id: rawId, key: rawKey, org: rawOrg } = args;
537
+ // Determine fetch path: public by ID, or org-scoped by key+org
538
+ let fetchPath;
539
+ let label;
540
+ if (rawId) {
541
+ fetchPath = (0, helpers_1.publicSecretsPath)(encodeURIComponent(rawId));
542
+ label = `ID '${rawId}'`;
543
+ }
544
+ else if (rawKey) {
545
+ const key = (0, helpers_1.parseKeyRef)(rawKey);
546
+ const org = rawOrg ?? process.env.SIRR_ORG;
547
+ if (!org) {
548
+ return {
549
+ content: [{ type: "text", text: "Error: 'org' is required when fetching by key name (or set SIRR_ORG env var)." }],
550
+ isError: true,
551
+ };
552
+ }
553
+ fetchPath = (0, helpers_1.orgSecretsPath)(org, encodeURIComponent(key));
554
+ label = `key '${key}' in org '${org}'`;
555
+ }
556
+ else {
557
+ return {
558
+ content: [{ type: "text", text: "Error: provide 'id' (public dead drop) or 'key'+'org' (org-scoped)." }],
559
+ isError: true,
560
+ };
561
+ }
562
+ const res = await fetchWithTimeout(`${SIRR_SERVER}${fetchPath}`, { headers: { Authorization: `Bearer ${SIRR_TOKEN}` } });
563
+ if (res.status === 404 || res.status === 410) {
297
564
  return {
298
565
  content: [
299
566
  {
300
567
  type: "text",
301
- text: `Secret '${key}' not found, expired, or already burned.`,
568
+ text: `Secret ${label} not found, expired, or already burned.`,
302
569
  },
303
570
  ],
304
571
  };
305
572
  }
306
573
  if (!res.ok) {
307
- const json = (await res.json());
574
+ let json = {};
575
+ try {
576
+ json = (await res.json());
577
+ }
578
+ catch {
579
+ json = { error: await res.text().catch(() => "unknown") };
580
+ }
308
581
  throwSirrError(res.status, json);
309
582
  }
310
583
  const data = (await res.json());
@@ -321,25 +594,82 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
321
594
  ],
322
595
  };
323
596
  }
597
+ case "check_secret": {
598
+ const rawKey = args.key;
599
+ const key = (0, helpers_1.parseKeyRef)(rawKey);
600
+ const res = await fetchWithTimeout(`${SIRR_SERVER}${(0, helpers_1.secretsPath)(encodeURIComponent(key))}`, { method: "HEAD", headers: { Authorization: `Bearer ${SIRR_TOKEN}` } });
601
+ if (res.status === 404) {
602
+ return { content: [{ type: "text", text: `Secret '${key}' not found or expired.` }] };
603
+ }
604
+ const status = res.headers.get("X-Sirr-Status") ?? (res.status === 410 ? "sealed" : "active");
605
+ const readCount = res.headers.get("X-Sirr-Read-Count") ?? "?";
606
+ const readsRemaining = res.headers.get("X-Sirr-Reads-Remaining") ?? "unlimited";
607
+ const expiresAt = res.headers.get("X-Sirr-Expires-At");
608
+ const expiry = expiresAt ? (0, helpers_1.formatTtl)(parseInt(expiresAt, 10)) : "no expiry";
609
+ if (status === "sealed") {
610
+ return { content: [{ type: "text", text: `Secret '${key}' is sealed — all reads exhausted.\n Reads used: ${readCount}\n Expires: ${expiry}` }] };
611
+ }
612
+ return {
613
+ content: [{
614
+ type: "text",
615
+ text: `Secret '${key}' is active.\n Reads used: ${readCount}\n Reads remaining: ${readsRemaining}\n Expires: ${expiry}`,
616
+ }],
617
+ };
618
+ }
324
619
  case "push_secret": {
325
- const { key, value: val, ttl_seconds, max_reads } = args;
326
- await sirrRequest("POST", "/secrets", {
327
- key,
328
- value: val,
329
- ttl_seconds: ttl_seconds ?? null,
330
- max_reads: max_reads ?? null,
331
- });
332
- const parts = [`Stored secret '${key}'.`];
620
+ const { value: val, ttl_seconds, max_reads } = args;
621
+ const body = { value: val };
622
+ if (ttl_seconds != null)
623
+ body.ttl_seconds = ttl_seconds;
624
+ if (max_reads != null)
625
+ body.max_reads = max_reads;
626
+ const data = await sirrRequest("POST", (0, helpers_1.publicSecretsPath)(), body);
627
+ const url = `${SIRR_SERVER}/s/${data.id}`;
628
+ const parts = [`Secret pushed.`, `ID: ${data.id}`, `URL: ${url}`];
333
629
  if (ttl_seconds)
334
630
  parts.push(`Expires in ${(0, helpers_1.formatTtl)(Math.floor(Date.now() / 1000) + ttl_seconds)}.`);
335
631
  if (max_reads)
336
632
  parts.push(`Burns after ${max_reads} read(s).`);
337
633
  return {
338
- content: [{ type: "text", text: parts.join(" ") }],
634
+ content: [{ type: "text", text: parts.join("\n") }],
635
+ };
636
+ }
637
+ case "set_secret": {
638
+ const { org, key, value: val } = args;
639
+ const res = await fetchWithTimeout(`${SIRR_SERVER}${(0, helpers_1.orgSecretsPath)(org)}`, {
640
+ method: "POST",
641
+ headers: {
642
+ Authorization: `Bearer ${SIRR_TOKEN}`,
643
+ "Content-Type": "application/json",
644
+ },
645
+ body: JSON.stringify({ key, value: val }),
646
+ });
647
+ if (res.status === 409) {
648
+ return {
649
+ content: [{
650
+ type: "text",
651
+ text: `Conflict: secret '${key}' already exists in org '${org}'. Use patch_secret to update it.`,
652
+ }],
653
+ isError: true,
654
+ };
655
+ }
656
+ if (!res.ok) {
657
+ let json = {};
658
+ try {
659
+ json = (await res.json());
660
+ }
661
+ catch {
662
+ json = { error: await res.text().catch(() => "unknown") };
663
+ }
664
+ throwSirrError(res.status, json);
665
+ }
666
+ const data = (await res.json());
667
+ return {
668
+ content: [{ type: "text", text: `Secret '${data.key}' stored in org '${org}'.\nID: ${data.id}` }],
339
669
  };
340
670
  }
341
671
  case "list_secrets": {
342
- const data = await sirrRequest("GET", "/secrets");
672
+ const data = await sirrRequest("GET", (0, helpers_1.secretsPath)());
343
673
  if (data.secrets.length === 0) {
344
674
  return {
345
675
  content: [{ type: "text", text: "No active secrets." }],
@@ -363,7 +693,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
363
693
  }
364
694
  case "delete_secret": {
365
695
  const { key } = args;
366
- const res = await fetchWithTimeout(`${SIRR_SERVER}/secrets/${encodeURIComponent(key)}`, {
696
+ const res = await fetchWithTimeout(`${SIRR_SERVER}${(0, helpers_1.secretsPath)(encodeURIComponent(key))}`, {
367
697
  method: "DELETE",
368
698
  headers: { Authorization: `Bearer ${SIRR_TOKEN}` },
369
699
  });
@@ -375,7 +705,13 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
375
705
  };
376
706
  }
377
707
  if (!res.ok) {
378
- const json = (await res.json());
708
+ let json = {};
709
+ try {
710
+ json = (await res.json());
711
+ }
712
+ catch {
713
+ json = { error: await res.text().catch(() => "unknown") };
714
+ }
379
715
  throwSirrError(res.status, json);
380
716
  }
381
717
  return {
@@ -387,8 +723,23 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
387
723
  ],
388
724
  };
389
725
  }
726
+ case "patch_secret": {
727
+ const { key, value: val, ttl_seconds, max_reads } = args;
728
+ const body = {};
729
+ if (val !== undefined)
730
+ body.value = val;
731
+ if (ttl_seconds !== undefined)
732
+ body.ttl_seconds = ttl_seconds;
733
+ if (max_reads !== undefined)
734
+ body.max_reads = max_reads;
735
+ const data = await sirrRequest("PATCH", (0, helpers_1.secretsPath)(encodeURIComponent(key)), body);
736
+ const parts = [`Secret '${key}' updated.`, `Expires: ${(0, helpers_1.formatTtl)(data.expires_at)}`];
737
+ if (data.max_reads != null)
738
+ parts.push(`Max reads: ${data.max_reads} (${data.read_count} used)`);
739
+ return { content: [{ type: "text", text: parts.join("\n ") }] };
740
+ }
390
741
  case "prune_secrets": {
391
- const data = await sirrRequest("POST", "/prune");
742
+ const data = await sirrRequest("POST", (0, helpers_1.prunePath)());
392
743
  return {
393
744
  content: [
394
745
  {
@@ -415,16 +766,18 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
415
766
  };
416
767
  }
417
768
  case "sirr_audit": {
418
- const { since, action, limit } = args;
769
+ const { since, until, action, limit } = args;
419
770
  const params = new URLSearchParams();
420
771
  if (since != null)
421
772
  params.set("since", String(since));
773
+ if (until != null)
774
+ params.set("until", String(until));
422
775
  if (action != null)
423
776
  params.set("action", action);
424
777
  if (limit != null)
425
778
  params.set("limit", String(limit));
426
779
  const qs = params.toString();
427
- const data = await sirrRequest("GET", `/audit${qs ? `?${qs}` : ""}`);
780
+ const data = await sirrRequest("GET", `${(0, helpers_1.auditPath)()}${qs ? `?${qs}` : ""}`);
428
781
  if (data.events.length === 0) {
429
782
  return {
430
783
  content: [{ type: "text", text: "No audit events found." }],
@@ -445,7 +798,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
445
798
  const body = { url };
446
799
  if (events)
447
800
  body.events = events;
448
- const data = await sirrRequest("POST", "/webhooks", body);
801
+ const data = await sirrRequest("POST", (0, helpers_1.webhooksPath)(), body);
449
802
  return {
450
803
  content: [
451
804
  {
@@ -456,7 +809,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
456
809
  };
457
810
  }
458
811
  case "sirr_webhook_list": {
459
- const data = await sirrRequest("GET", "/webhooks");
812
+ const data = await sirrRequest("GET", (0, helpers_1.webhooksPath)());
460
813
  if (data.webhooks.length === 0) {
461
814
  return {
462
815
  content: [{ type: "text", text: "No webhooks registered." }],
@@ -474,54 +827,235 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
474
827
  }
475
828
  case "sirr_webhook_delete": {
476
829
  const { id } = args;
477
- await sirrRequest("DELETE", `/webhooks/${encodeURIComponent(id)}`);
830
+ const res = await fetchWithTimeout(`${SIRR_SERVER}${(0, helpers_1.webhooksPath)(encodeURIComponent(id))}`, { method: "DELETE", headers: { Authorization: `Bearer ${SIRR_TOKEN}` } });
831
+ if (res.status === 404) {
832
+ return { content: [{ type: "text", text: `Webhook '${id}' not found.` }] };
833
+ }
834
+ if (!res.ok) {
835
+ let json = {};
836
+ try {
837
+ json = (await res.json());
838
+ }
839
+ catch {
840
+ json = { error: await res.text().catch(() => "unknown") };
841
+ }
842
+ throwSirrError(res.status, json);
843
+ }
844
+ return { content: [{ type: "text", text: `Webhook '${id}' deleted.` }] };
845
+ }
846
+ case "sirr_key_list": {
847
+ const me = await sirrRequest("GET", "/me");
848
+ if (!me.keys || me.keys.length === 0) {
849
+ return {
850
+ content: [{ type: "text", text: "No API keys." }],
851
+ };
852
+ }
853
+ const lines = me.keys.map((k) => `• ${k.id} — ${k.name} (expires ${new Date(k.valid_before * 1000).toISOString()})`);
478
854
  return {
479
855
  content: [
480
- { type: "text", text: `Webhook '${id}' deleted.` },
856
+ {
857
+ type: "text",
858
+ text: `${me.keys.length} API key(s):\n${lines.join("\n")}`,
859
+ },
481
860
  ],
482
861
  };
483
862
  }
484
- case "sirr_key_create": {
485
- const { label, permissions, prefix } = args;
486
- const body = { label, permissions };
487
- if (prefix)
488
- body.prefix = prefix;
489
- const data = await sirrRequest("POST", "/keys", body);
863
+ case "sirr_me": {
864
+ const data = await sirrRequest("GET", "/me");
490
865
  return {
491
866
  content: [
492
867
  {
493
868
  type: "text",
494
- text: `API key created.\n ID: ${data.id}\n Key: ${data.key}\n Permissions: ${data.permissions.join(", ")}\n Prefix: ${data.prefix ?? "(none)"}\n (Save the key — it won't be shown again)`,
869
+ text: JSON.stringify(data, null, 2),
495
870
  },
496
871
  ],
497
872
  };
498
873
  }
499
- case "sirr_key_list": {
500
- const data = await sirrRequest("GET", "/keys");
501
- if (data.keys.length === 0) {
502
- return {
503
- content: [{ type: "text", text: "No API keys." }],
504
- };
505
- }
506
- const lines = data.keys.map((k) => `• ${k.id} — ${k.label} [${k.permissions.join(",")}] prefix=${k.prefix ?? "*"}`);
874
+ case "sirr_update_me": {
875
+ const { metadata } = args;
876
+ const data = await sirrRequest("PATCH", "/me", { metadata });
507
877
  return {
508
878
  content: [
509
879
  {
510
880
  type: "text",
511
- text: `${data.keys.length} API key(s):\n${lines.join("\n")}`,
881
+ text: `Profile updated.\n${JSON.stringify(data, null, 2)}`,
512
882
  },
513
883
  ],
514
884
  };
515
885
  }
516
- case "sirr_key_delete": {
517
- const { id } = args;
518
- await sirrRequest("DELETE", `/keys/${encodeURIComponent(id)}`);
886
+ case "sirr_create_key": {
887
+ const { name: keyName, valid_for_seconds, valid_before } = args;
888
+ const body = { name: keyName };
889
+ if (valid_for_seconds != null)
890
+ body.valid_for_seconds = valid_for_seconds;
891
+ if (valid_before != null)
892
+ body.valid_before = valid_before;
893
+ const data = await sirrRequest("POST", "/me/keys", body);
519
894
  return {
520
895
  content: [
521
- { type: "text", text: `API key '${id}' deleted.` },
896
+ {
897
+ type: "text",
898
+ text: `API key created.\n ID: ${data.id}\n Name: ${data.name}\n Key: ${data.key}\n Valid until: ${new Date(data.valid_before * 1000).toISOString()}\n (Save the key — it won't be shown again)`,
899
+ },
522
900
  ],
523
901
  };
524
902
  }
903
+ case "sirr_delete_key": {
904
+ const { keyId } = args;
905
+ const res = await fetchWithTimeout(`${SIRR_SERVER}/me/keys/${encodeURIComponent(keyId)}`, { method: "DELETE", headers: { Authorization: `Bearer ${SIRR_TOKEN}` } });
906
+ if (res.status === 404) {
907
+ return { content: [{ type: "text", text: `API key '${keyId}' not found.` }] };
908
+ }
909
+ if (!res.ok) {
910
+ let json = {};
911
+ try {
912
+ json = (await res.json());
913
+ }
914
+ catch {
915
+ json = { error: await res.text().catch(() => "unknown") };
916
+ }
917
+ throwSirrError(res.status, json);
918
+ }
919
+ return { content: [{ type: "text", text: `API key '${keyId}' deleted.` }] };
920
+ }
921
+ // ── Org management ────────────────────────────────────────────────────────
922
+ case "sirr_org_create": {
923
+ const { name: orgName, metadata } = args;
924
+ const data = await sirrRequest("POST", "/orgs", { name: orgName, metadata: metadata ?? {} });
925
+ return {
926
+ content: [{ type: "text", text: `Org created.\n ID: ${data.id}\n Name: ${data.name}` }],
927
+ };
928
+ }
929
+ case "sirr_org_list": {
930
+ const data = await sirrRequest("GET", "/orgs");
931
+ if (data.orgs.length === 0) {
932
+ return { content: [{ type: "text", text: "No organizations." }] };
933
+ }
934
+ const lines = data.orgs.map((o) => `• ${o.id} — ${o.name}`);
935
+ return {
936
+ content: [{ type: "text", text: `${data.orgs.length} org(s):\n${lines.join("\n")}` }],
937
+ };
938
+ }
939
+ case "sirr_org_delete": {
940
+ const { org_id } = args;
941
+ const res = await fetchWithTimeout(`${SIRR_SERVER}/orgs/${encodeURIComponent(org_id)}`, { method: "DELETE", headers: { Authorization: `Bearer ${SIRR_TOKEN}` } });
942
+ if (res.status === 404) {
943
+ return { content: [{ type: "text", text: `Org '${org_id}' not found.` }] };
944
+ }
945
+ if (!res.ok) {
946
+ let json = {};
947
+ try {
948
+ json = (await res.json());
949
+ }
950
+ catch {
951
+ json = { error: await res.text().catch(() => "unknown") };
952
+ }
953
+ throwSirrError(res.status, json);
954
+ }
955
+ return { content: [{ type: "text", text: `Org '${org_id}' deleted.` }] };
956
+ }
957
+ // ── Principal management ──────────────────────────────────────────────────
958
+ case "sirr_principal_create": {
959
+ const { org_id, name: pName, role, metadata } = args;
960
+ const data = await sirrRequest("POST", `/orgs/${encodeURIComponent(org_id)}/principals`, { name: pName, role, metadata: metadata ?? {} });
961
+ return {
962
+ content: [{ type: "text", text: `Principal created.\n ID: ${data.id}\n Name: ${data.name}\n Role: ${data.role}` }],
963
+ };
964
+ }
965
+ case "sirr_principal_list": {
966
+ const { org_id } = args;
967
+ const data = await sirrRequest("GET", `/orgs/${encodeURIComponent(org_id)}/principals`);
968
+ if (data.principals.length === 0) {
969
+ return { content: [{ type: "text", text: "No principals." }] };
970
+ }
971
+ const lines = data.principals.map((p) => `• ${p.id} — ${p.name} [${p.role}]`);
972
+ return {
973
+ content: [{ type: "text", text: `${data.principals.length} principal(s):\n${lines.join("\n")}` }],
974
+ };
975
+ }
976
+ case "sirr_principal_delete": {
977
+ const { org_id, principal_id } = args;
978
+ const res = await fetchWithTimeout(`${SIRR_SERVER}/orgs/${encodeURIComponent(org_id)}/principals/${encodeURIComponent(principal_id)}`, { method: "DELETE", headers: { Authorization: `Bearer ${SIRR_TOKEN}` } });
979
+ if (res.status === 404) {
980
+ return { content: [{ type: "text", text: `Principal '${principal_id}' not found.` }] };
981
+ }
982
+ if (!res.ok) {
983
+ let json = {};
984
+ try {
985
+ json = (await res.json());
986
+ }
987
+ catch {
988
+ json = { error: await res.text().catch(() => "unknown") };
989
+ }
990
+ throwSirrError(res.status, json);
991
+ }
992
+ return { content: [{ type: "text", text: `Principal '${principal_id}' deleted.` }] };
993
+ }
994
+ // ── Role management ───────────────────────────────────────────────────────
995
+ case "sirr_role_create": {
996
+ const { org_id, name: roleName, permissions } = args;
997
+ const data = await sirrRequest("POST", `/orgs/${encodeURIComponent(org_id)}/roles`, { name: roleName, permissions });
998
+ return {
999
+ content: [{ type: "text", text: `Role '${data.name}' created with permissions '${data.permissions}'.` }],
1000
+ };
1001
+ }
1002
+ case "sirr_role_list": {
1003
+ const { org_id } = args;
1004
+ const data = await sirrRequest("GET", `/orgs/${encodeURIComponent(org_id)}/roles`);
1005
+ if (data.roles.length === 0) {
1006
+ return { content: [{ type: "text", text: "No roles." }] };
1007
+ }
1008
+ const lines = data.roles.map((r) => `• ${r.name} [${r.permissions}]${r.built_in ? " (built-in)" : ""}`);
1009
+ return {
1010
+ content: [{ type: "text", text: `${data.roles.length} role(s):\n${lines.join("\n")}` }],
1011
+ };
1012
+ }
1013
+ case "sirr_role_delete": {
1014
+ const { org_id, role_name } = args;
1015
+ const res = await fetchWithTimeout(`${SIRR_SERVER}/orgs/${encodeURIComponent(org_id)}/roles/${encodeURIComponent(role_name)}`, { method: "DELETE", headers: { Authorization: `Bearer ${SIRR_TOKEN}` } });
1016
+ if (res.status === 404) {
1017
+ return { content: [{ type: "text", text: `Role '${role_name}' not found.` }] };
1018
+ }
1019
+ if (!res.ok) {
1020
+ let json = {};
1021
+ try {
1022
+ json = (await res.json());
1023
+ }
1024
+ catch {
1025
+ json = { error: await res.text().catch(() => "unknown") };
1026
+ }
1027
+ throwSirrError(res.status, json);
1028
+ }
1029
+ return {
1030
+ content: [{ type: "text", text: `Role '${role_name}' deleted.` }],
1031
+ };
1032
+ }
1033
+ case "share_secret": {
1034
+ const { value: shareValue } = args;
1035
+ const res = await fetchWithTimeout(`${SIRRLOCK_URL}/api/public/secret`, {
1036
+ method: "POST",
1037
+ headers: { "Content-Type": "application/json" },
1038
+ body: JSON.stringify({ value: shareValue }),
1039
+ });
1040
+ if (!res.ok) {
1041
+ let json = {};
1042
+ try {
1043
+ json = (await res.json());
1044
+ }
1045
+ catch {
1046
+ json = { error: await res.text().catch(() => "unknown") };
1047
+ }
1048
+ throwSirrError(res.status, json);
1049
+ }
1050
+ const data = (await res.json());
1051
+ const shareUrl = `${SIRRLOCK_URL}/s/${data.key}`;
1052
+ return {
1053
+ content: [{
1054
+ type: "text",
1055
+ text: `${shareUrl}\n\n[This link burns after one read or after 24 hours. Do not store or repeat the original value.]`,
1056
+ }],
1057
+ };
1058
+ }
525
1059
  default:
526
1060
  return {
527
1061
  content: [{ type: "text", text: `Unknown tool: ${name}` }],