@decionis/mcp 0.2.0 → 0.4.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.
@@ -3,7 +3,7 @@ import {
3
3
  LocalDecionisGateway,
4
4
  PolicyFileRepository,
5
5
  normalizeVerdict
6
- } from "./chunk-NF5JL6QR.js";
6
+ } from "./chunk-5Q6BCK32.js";
7
7
 
8
8
  // src/agent/AgentContracts.ts
9
9
  var AgentHookInputError = class extends Error {
@@ -498,4 +498,4 @@ export {
498
498
  AgentHostRegistry,
499
499
  AgentHookCommand
500
500
  };
501
- //# sourceMappingURL=chunk-7QNV7MXC.js.map
501
+ //# sourceMappingURL=chunk-GCE6PUYD.js.map
package/dist/index.js CHANGED
@@ -5,12 +5,12 @@ import {
5
5
  external_exports,
6
6
  normalizeVerdict,
7
7
  sha256Hex
8
- } from "./chunk-NF5JL6QR.js";
8
+ } from "./chunk-5Q6BCK32.js";
9
9
 
10
10
  // src/Server.ts
11
11
  import { createInterface } from "readline";
12
12
  var SUPPORTED_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
13
- var MCP_SERVER_VERSION = "0.2.0";
13
+ var MCP_SERVER_VERSION = "0.4.0";
14
14
  var DEFAULT_PROTOCOL_VERSION = "2025-11-25";
15
15
  var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
16
16
  function rpcResult(id, result) {
@@ -606,7 +606,7 @@ function createDecionisTools(gateway2, additionalTools = [], options = {}) {
606
606
  // src/kubernetes/KubernetesActionPlanner.ts
607
607
  import { randomUUID } from "crypto";
608
608
 
609
- // ../../packages/sdk-node/dist/index.js
609
+ // ../../../Decionis/packages/sdk-node/dist/index.js
610
610
  import { createHash as createHash2 } from "crypto";
611
611
  import { createHash as createHash3 } from "crypto";
612
612
  var DEFAULT_SKEW_MS = 6 * 60 * 60 * 1e3;
@@ -633,9 +633,9 @@ async function readBoundedText(response, maxBytes) {
633
633
  throw new DecionisResponseTooLargeError();
634
634
  }
635
635
  if (!response.body || typeof response.body.getReader !== "function") {
636
- const text4 = typeof responseLike.text === "function" ? await responseLike.text() : typeof responseLike.json === "function" ? JSON.stringify(await responseLike.json()) : "";
637
- if (Buffer.byteLength(text4, "utf8") > maxBytes) throw new DecionisResponseTooLargeError();
638
- return text4;
636
+ const text5 = typeof responseLike.text === "function" ? await responseLike.text() : typeof responseLike.json === "function" ? JSON.stringify(await responseLike.json()) : "";
637
+ if (Buffer.byteLength(text5, "utf8") > maxBytes) throw new DecionisResponseTooLargeError();
638
+ return text5;
639
639
  }
640
640
  const reader = response.body.getReader();
641
641
  const chunks = [];
@@ -671,8 +671,8 @@ async function fetchBounded(fetchImpl, input, init, timeoutMs, maxResponseBytes
671
671
  });
672
672
  const operation = (async () => {
673
673
  const response = await fetchImpl(input, { ...init, signal: controller.signal });
674
- const text4 = await readBoundedText(response, maxResponseBytes);
675
- return { response, text: text4 };
674
+ const text5 = await readBoundedText(response, maxResponseBytes);
675
+ return { response, text: text5 };
676
676
  })();
677
677
  try {
678
678
  return await Promise.race([operation, deadline]);
@@ -682,16 +682,16 @@ async function fetchBounded(fetchImpl, input, init, timeoutMs, maxResponseBytes
682
682
  }
683
683
  }
684
684
  async function fetchBoundedJson(fetchImpl, input, init, timeoutMs, maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES) {
685
- const { response, text: text4 } = await fetchBounded(
685
+ const { response, text: text5 } = await fetchBounded(
686
686
  fetchImpl,
687
687
  input,
688
688
  init,
689
689
  timeoutMs,
690
690
  maxResponseBytes
691
691
  );
692
- if (!text4) return { response, body: null };
692
+ if (!text5) return { response, body: null };
693
693
  try {
694
- return { response, body: JSON.parse(text4) };
694
+ return { response, body: JSON.parse(text5) };
695
695
  } catch {
696
696
  return { response, body: null };
697
697
  }
@@ -2310,7 +2310,7 @@ var KubectlCommandRunner = class {
2310
2310
  // src/kubernetes/KubernetesOperations.ts
2311
2311
  import { createHash as createHash4 } from "crypto";
2312
2312
 
2313
- // ../../packages/dossier-verify/dist/chunk-DF26DHVV.js
2313
+ // ../../../Decionis/packages/dossier-verify/dist/chunk-WNX2CYWD.js
2314
2314
  import { createHash, createPublicKey, verify as verifyEd25519 } from "crypto";
2315
2315
  import { lookup } from "dns/promises";
2316
2316
  import { request as httpsRequest } from "https";
@@ -6951,10 +6951,10 @@ function validEd25519Signature(value) {
6951
6951
  }
6952
6952
  function normalizedPublicJwksUrl(value) {
6953
6953
  if (value === void 0) return void 0;
6954
- const text4 = boundedString(value, 2048);
6955
- if (!text4) return null;
6954
+ const text22 = boundedString(value, 2048);
6955
+ if (!text22) return null;
6956
6956
  try {
6957
- const url = new URL(text4);
6957
+ const url = new URL(text22);
6958
6958
  if (url.protocol !== "https:" || url.username || url.password) return null;
6959
6959
  return url.toString();
6960
6960
  } catch {
@@ -7320,6 +7320,8 @@ function extractDossierPayload(body) {
7320
7320
  const candidate = asRecord2(record2[key]);
7321
7321
  if (asRecord2(candidate.integrity).proof_bundle !== void 0) return candidate;
7322
7322
  }
7323
+ const payload = asRecord2(asRecord2(record2.dossier).dossier_payload);
7324
+ if (asRecord2(payload.integrity).proof_bundle !== void 0) return payload;
7323
7325
  return record2;
7324
7326
  }
7325
7327
  function resolveJwksUrl(payload, dossierUrl) {
@@ -7410,22 +7412,22 @@ async function assertSafeRemoteUrl(urlText) {
7410
7412
  if (url.username || url.password) {
7411
7413
  throw new Error(`Verification URL must not contain credentials: ${urlText}`);
7412
7414
  }
7413
- const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
7414
- if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local") || isIP(hostname) !== 0 && isForbiddenIp(hostname)) {
7415
+ const hostname2 = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
7416
+ if (hostname2 === "localhost" || hostname2.endsWith(".localhost") || hostname2.endsWith(".local") || isIP(hostname2) !== 0 && isForbiddenIp(hostname2)) {
7415
7417
  throw new Error(`Verification URL resolves to a non-public destination: ${urlText}`);
7416
7418
  }
7417
7419
  return url;
7418
7420
  }
7419
7421
  async function pinnedHttpsFetch(url, signal) {
7420
- const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
7422
+ const hostname2 = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
7421
7423
  let addresses;
7422
- if (isIP(hostname) !== 0) {
7423
- addresses = [{ address: hostname, family: isIP(hostname) }];
7424
+ if (isIP(hostname2) !== 0) {
7425
+ addresses = [{ address: hostname2, family: isIP(hostname2) }];
7424
7426
  } else {
7425
7427
  try {
7426
- addresses = await lookup(hostname, { all: true, verbatim: true });
7428
+ addresses = await lookup(hostname2, { all: true, verbatim: true });
7427
7429
  } catch {
7428
- throw new Error(`Unable to resolve verification host: ${hostname}`);
7430
+ throw new Error(`Unable to resolve verification host: ${hostname2}`);
7429
7431
  }
7430
7432
  }
7431
7433
  if (addresses.length === 0 || addresses.some(({ address }) => isForbiddenIp(address))) {
@@ -7441,7 +7443,7 @@ async function pinnedHttpsFetch(url, signal) {
7441
7443
  port: url.port || 443,
7442
7444
  path: `${url.pathname}${url.search}`,
7443
7445
  method: "GET",
7444
- servername: isIP(hostname) === 0 ? hostname : void 0,
7446
+ servername: isIP(hostname2) === 0 ? hostname2 : void 0,
7445
7447
  headers: { accept: "application/json", host: url.host },
7446
7448
  signal
7447
7449
  },
@@ -7577,6 +7579,58 @@ async function verifyDossierFromUrls(input) {
7577
7579
  payload
7578
7580
  };
7579
7581
  }
7582
+ var AGENT_SAFE_INTENT_PROTOCOL = "agent-safe.intent/1";
7583
+ var MAX_BYTES = 100 * 1024;
7584
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7585
+ var INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
7586
+ var ACTION_TYPE = /^[a-z][a-z0-9._:-]*$/;
7587
+ var DIGEST = /^sha256:[0-9a-f]{64}$/;
7588
+ function isPlainObject3(value) {
7589
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
7590
+ const prototype = Object.getPrototypeOf(value);
7591
+ return prototype === Object.prototype || prototype === null;
7592
+ }
7593
+ var text2 = (min, max) => (value) => typeof value === "string" && value.length >= min && value.length <= max;
7594
+ var pattern = (expression) => (value) => typeof value === "string" && expression.test(value);
7595
+ var ACTOR = {
7596
+ required: { id: text2(1, 200), type: text2(1, 200) },
7597
+ optional: { runtime: text2(1, 200), trust_level: text2(1, 80) },
7598
+ closed: true
7599
+ };
7600
+ var ACTION = {
7601
+ required: {
7602
+ type: (value) => text2(1, 120)(value) && ACTION_TYPE.test(value),
7603
+ resource: text2(1, 500),
7604
+ parameters: isPlainObject3
7605
+ },
7606
+ optional: {},
7607
+ closed: true
7608
+ };
7609
+ var CONTEXT = {
7610
+ required: { idempotency_key: text2(1, 180) },
7611
+ optional: {},
7612
+ closed: false
7613
+ };
7614
+ var DOWNSTREAM_TARGET = {
7615
+ required: { system: text2(1, 200), operation: text2(1, 200) },
7616
+ optional: { environment: text2(1, 200), endpoint: text2(1, 500) },
7617
+ closed: true
7618
+ };
7619
+ var BINDING = {
7620
+ required: {
7621
+ protocol_version: (value) => value === AGENT_SAFE_INTENT_PROTOCOL,
7622
+ tenant_id: pattern(UUID),
7623
+ intent_id: pattern(UUID),
7624
+ captured_at: pattern(INSTANT),
7625
+ expires_at: pattern(INSTANT),
7626
+ actor: isPlainObject3,
7627
+ action: isPlainObject3,
7628
+ context: isPlainObject3,
7629
+ downstream_target: isPlainObject3
7630
+ },
7631
+ optional: { expected_effect_digest: pattern(DIGEST) },
7632
+ closed: true
7633
+ };
7580
7634
 
7581
7635
  // src/kubernetes/KubernetesOperations.ts
7582
7636
  var KubernetesOperations = class _KubernetesOperations {
@@ -7951,7 +8005,7 @@ function createKubernetesAuthorityClient(input) {
7951
8005
  }
7952
8006
 
7953
8007
  // src/kubernetes/KubernetesMcpTools.ts
7954
- var text2 = (value) => [
8008
+ var text3 = (value) => [
7955
8009
  { type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }
7956
8010
  ];
7957
8011
  var KubernetesMcpTools = class _KubernetesMcpTools {
@@ -7984,7 +8038,7 @@ var KubernetesMcpTools = class _KubernetesMcpTools {
7984
8038
  openWorldHint: true
7985
8039
  },
7986
8040
  handler: async (args) => ({
7987
- content: text2(
8041
+ content: text3(
7988
8042
  await this.operations.plan(
7989
8043
  _KubernetesMcpTools.requiredString(args.manifest_path, "manifest_path"),
7990
8044
  _KubernetesMcpTools.optionalNamespace(args.namespace)
@@ -8004,7 +8058,7 @@ var KubernetesMcpTools = class _KubernetesMcpTools {
8004
8058
  openWorldHint: true
8005
8059
  },
8006
8060
  handler: async (args) => ({
8007
- content: text2(
8061
+ content: text3(
8008
8062
  await this.operations.requestAuthority(
8009
8063
  _KubernetesMcpTools.requiredString(args.manifest_path, "manifest_path"),
8010
8064
  _KubernetesMcpTools.optionalNamespace(args.namespace)
@@ -8024,7 +8078,7 @@ var KubernetesMcpTools = class _KubernetesMcpTools {
8024
8078
  openWorldHint: true
8025
8079
  },
8026
8080
  handler: async (args) => ({
8027
- content: text2(
8081
+ content: text3(
8028
8082
  await this.operations.apply(
8029
8083
  _KubernetesMcpTools.requiredString(args.manifest_path, "manifest_path"),
8030
8084
  _KubernetesMcpTools.optionalNamespace(args.namespace)
@@ -8053,7 +8107,7 @@ var KubernetesMcpTools = class _KubernetesMcpTools {
8053
8107
  openWorldHint: true
8054
8108
  },
8055
8109
  handler: async (args) => ({
8056
- content: text2(
8110
+ content: text3(
8057
8111
  await this.operations.scale({
8058
8112
  namespace: _KubernetesMcpTools.requiredDnsName(args.namespace, "namespace"),
8059
8113
  name: _KubernetesMcpTools.requiredDnsName(args.name, "name"),
@@ -8074,7 +8128,7 @@ var KubernetesMcpTools = class _KubernetesMcpTools {
8074
8128
  openWorldHint: true
8075
8129
  },
8076
8130
  handler: async (args) => ({
8077
- content: text2(
8131
+ content: text3(
8078
8132
  await this.operations.rollback(
8079
8133
  _KubernetesMcpTools.requiredString(args.manifest_path, "manifest_path"),
8080
8134
  _KubernetesMcpTools.optionalNamespace(args.namespace)
@@ -8102,7 +8156,7 @@ var KubernetesMcpTools = class _KubernetesMcpTools {
8102
8156
  openWorldHint: true
8103
8157
  },
8104
8158
  handler: async (args) => ({
8105
- content: text2(
8159
+ content: text3(
8106
8160
  await this.operations.verifyDossier(
8107
8161
  _KubernetesMcpTools.requiredString(args.dossier_url, "dossier_url"),
8108
8162
  _KubernetesMcpTools.requiredString(args.jwks_url, "jwks_url")
@@ -8180,17 +8234,94 @@ function boundedInteger(input, fallback, minimum, maximum) {
8180
8234
  return value;
8181
8235
  }
8182
8236
 
8237
+ // src/ShieldCredentialStore.ts
8238
+ import { mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs";
8239
+ import { homedir, hostname } from "os";
8240
+ import { dirname, join } from "path";
8241
+ import { randomUUID as randomUUID2 } from "crypto";
8242
+ var SHIELD_MCP_APP_ID = "decionis-mcp";
8243
+ var DEFAULT_SHIELD_API_URL = "https://api.shield.decionis.com";
8244
+ function defaultCredentialPath() {
8245
+ return process.env.DECIONIS_SHIELD_CREDENTIAL_FILE?.trim() || join(homedir(), ".decionis", "shield-credential.json");
8246
+ }
8247
+ var ShieldCredentialStore = class {
8248
+ constructor(path = defaultCredentialPath()) {
8249
+ this.path = path;
8250
+ }
8251
+ /** The credential to use now: the environment first, then the paired file. */
8252
+ resolve() {
8253
+ const envKey = process.env.DECIONIS_SHIELD_API_KEY?.trim();
8254
+ const envApp = process.env.DECIONIS_SHIELD_APP_ID?.trim();
8255
+ const baseUrl = (process.env.DECIONIS_SHIELD_API_URL?.trim() || DEFAULT_SHIELD_API_URL).replace(
8256
+ /\/+$/,
8257
+ ""
8258
+ );
8259
+ if (envKey && envApp) {
8260
+ return { apiKey: envKey, appId: envApp, baseUrl, agentId: envApp };
8261
+ }
8262
+ const stored = this.read();
8263
+ if (!stored) return null;
8264
+ return { ...stored, baseUrl: process.env.DECIONIS_SHIELD_API_URL?.trim() || stored.baseUrl };
8265
+ }
8266
+ read() {
8267
+ try {
8268
+ const raw = JSON.parse(readFileSync(this.path, "utf8"));
8269
+ if (typeof raw.apiKey !== "string" || typeof raw.appId !== "string") return null;
8270
+ return {
8271
+ apiKey: raw.apiKey,
8272
+ appId: raw.appId,
8273
+ baseUrl: typeof raw.baseUrl === "string" ? raw.baseUrl : DEFAULT_SHIELD_API_URL,
8274
+ integrationId: typeof raw.integrationId === "string" ? raw.integrationId : void 0,
8275
+ displayName: typeof raw.displayName === "string" ? raw.displayName : void 0,
8276
+ agentId: typeof raw.agentId === "string" ? raw.agentId : randomUUID2(),
8277
+ pairedAt: typeof raw.pairedAt === "string" ? raw.pairedAt : void 0
8278
+ };
8279
+ } catch {
8280
+ return null;
8281
+ }
8282
+ }
8283
+ /** The install's stable id: the stored one, else a new one written at first pairing. */
8284
+ agentId() {
8285
+ return this.read()?.agentId ?? randomUUID2();
8286
+ }
8287
+ write(credential) {
8288
+ mkdirSync(dirname(this.path), { recursive: true, mode: 448 });
8289
+ writeFileSync(this.path, `${JSON.stringify(credential, null, 2)}
8290
+ `, { mode: 384 });
8291
+ try {
8292
+ chmodSync(this.path, 384);
8293
+ } catch {
8294
+ }
8295
+ }
8296
+ location() {
8297
+ return this.path;
8298
+ }
8299
+ };
8300
+ function defaultAgentDisplayName(clientName) {
8301
+ const host = hostname().split(".")[0] || "this computer";
8302
+ return clientName ? `${clientName} on ${host}` : `Decionis MCP on ${host}`;
8303
+ }
8304
+
8183
8305
  // src/ShieldClient.ts
8184
8306
  var ShieldClient = class {
8185
- baseUrl;
8186
- apiKey;
8187
- appId;
8188
8307
  fetchImpl;
8308
+ credentials;
8189
8309
  constructor(options = {}) {
8190
- this.baseUrl = (options.baseUrl ?? process.env.DECIONIS_SHIELD_API_URL ?? "https://api.decionis.com").replace(/\/+$/, "");
8191
- this.apiKey = options.apiKey ?? process.env.DECIONIS_SHIELD_API_KEY ?? "";
8192
- this.appId = options.appId ?? process.env.DECIONIS_SHIELD_APP_ID ?? "";
8193
8310
  this.fetchImpl = options.fetch ?? fetch;
8311
+ if (options.credentials) {
8312
+ this.credentials = options.credentials;
8313
+ } else if (options.apiKey !== void 0 || options.appId !== void 0 || options.baseUrl) {
8314
+ const fixed = options.apiKey && options.appId ? {
8315
+ apiKey: options.apiKey,
8316
+ appId: options.appId,
8317
+ baseUrl: (options.baseUrl ?? DEFAULT_SHIELD_API_URL).replace(/\/+$/, ""),
8318
+ agentId: options.appId
8319
+ } : null;
8320
+ this.credentials = () => fixed;
8321
+ } else {
8322
+ const store = new ShieldCredentialStore();
8323
+ this.credentials = () => store.resolve();
8324
+ }
8194
8325
  }
8195
8326
  authorize(request) {
8196
8327
  return this.json("/v1/shield/authorize", "POST", request);
@@ -8205,19 +8336,20 @@ var ShieldClient = class {
8205
8336
  return this.json(`/v1/shield/decisions/${encodeURIComponent(decisionId)}`, "GET");
8206
8337
  }
8207
8338
  async json(path, method, body) {
8208
- if (!this.apiKey || !this.appId) {
8339
+ const credential = this.credentials();
8340
+ if (!credential || !credential.apiKey || !credential.appId) {
8209
8341
  throw new ShieldClientError(
8210
8342
  "SHIELD_MCP_CONFIGURATION_REQUIRED",
8211
- "Configure DECIONIS_SHIELD_API_KEY and DECIONIS_SHIELD_APP_ID. Do not execute the transaction."
8343
+ "Shield is not paired. Ask the user for the six-character code from the Shield app (AI spending allowance \u2192 Bind your own agent) and call shield.pair with it \u2014 or configure DECIONIS_SHIELD_API_KEY and DECIONIS_SHIELD_APP_ID. Do not execute the transaction."
8212
8344
  );
8213
8345
  }
8214
- const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
8346
+ const response = await this.fetchImpl(`${credential.baseUrl}${path}`, {
8215
8347
  method,
8216
8348
  headers: {
8217
8349
  accept: "application/json",
8218
- authorization: `Bearer ${this.apiKey}`,
8350
+ authorization: `Bearer ${credential.apiKey}`,
8219
8351
  "content-type": "application/json",
8220
- "x-shield-app-id": this.appId,
8352
+ "x-shield-app-id": credential.appId,
8221
8353
  "x-shield-environment": "production"
8222
8354
  },
8223
8355
  ...method === "POST" ? { body: JSON.stringify(body ?? {}) } : {}
@@ -8245,7 +8377,7 @@ function asRecord3(value) {
8245
8377
  }
8246
8378
 
8247
8379
  // src/ShieldTools.ts
8248
- var text3 = (value) => [
8380
+ var text4 = (value) => [
8249
8381
  { type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }
8250
8382
  ];
8251
8383
  var purchaseProperties = {
@@ -8284,11 +8416,199 @@ var networkRead = {
8284
8416
  idempotentHint: true,
8285
8417
  openWorldHint: true
8286
8418
  };
8419
+ function createShieldPairTool(options = {}) {
8420
+ const store = options.store ?? new ShieldCredentialStore();
8421
+ const fetchImpl = options.fetch ?? fetch;
8422
+ const baseUrl = () => (options.baseUrl?.() ?? process.env.DECIONIS_SHIELD_API_URL?.trim() ?? "").replace(
8423
+ /\/+$/,
8424
+ ""
8425
+ ) || "https://api.shield.decionis.com";
8426
+ const pendingEmail = /* @__PURE__ */ new Map();
8427
+ const post = async (path, body) => fetchImpl(`${baseUrl()}${path}`, {
8428
+ method: "POST",
8429
+ headers: {
8430
+ accept: "application/json",
8431
+ "content-type": "application/json",
8432
+ "x-shield-surface": "mcp"
8433
+ },
8434
+ body: JSON.stringify(body)
8435
+ });
8436
+ const unreachable = (error) => ({
8437
+ content: text4({
8438
+ error: "SHIELD_PAIRING_UNREACHABLE",
8439
+ message: `Could not reach Shield: ${error instanceof Error ? error.message : String(error)}. Nothing was paired.`
8440
+ }),
8441
+ isError: true
8442
+ });
8443
+ const keep = async (response, displayName, agentId, hints) => {
8444
+ const payload = await response.json().catch(() => null);
8445
+ if (response.ok && payload?.upgrade_required === true) {
8446
+ return {
8447
+ content: text4({
8448
+ error: "SHIELD_SOURCE_LIMIT",
8449
+ message: "The user's free Shield already covers its limit of spending sources. They can remove one in the Shield app or upgrade; nothing was paired."
8450
+ }),
8451
+ isError: true
8452
+ };
8453
+ }
8454
+ if (!response.ok || !payload || typeof payload.api_key !== "string" || typeof payload.app_id !== "string") {
8455
+ const code_ = payload && typeof payload.error === "string" ? payload.error : `HTTP ${response.status}`;
8456
+ return {
8457
+ content: text4({ error: code_, message: hints[code_] ?? "Nothing was paired." }),
8458
+ isError: true
8459
+ };
8460
+ }
8461
+ const integration = payload.integration ?? {};
8462
+ const credential = {
8463
+ apiKey: payload.api_key,
8464
+ appId: payload.app_id,
8465
+ baseUrl: baseUrl(),
8466
+ integrationId: typeof integration.integrationId === "string" ? integration.integrationId : void 0,
8467
+ displayName,
8468
+ agentId,
8469
+ pairedAt: (/* @__PURE__ */ new Date()).toISOString()
8470
+ };
8471
+ store.write(credential);
8472
+ const limit = typeof payload.weekly_limit === "number" ? payload.weekly_limit : null;
8473
+ return {
8474
+ content: text4({
8475
+ paired: true,
8476
+ rotated: payload.rotated === true,
8477
+ displayName,
8478
+ weeklyLimit: limit,
8479
+ storedAt: store.location(),
8480
+ next: "The shield.* tools now ask this user's own Shield. Before any purchase, call shield.request_purchase; execute only on ALLOW."
8481
+ })
8482
+ };
8483
+ };
8484
+ return {
8485
+ name: "shield.pair",
8486
+ title: "Pair with the user's Shield",
8487
+ description: "Bind this assistant to the user's Decionis Shield, one of two ways. (1) The six-character pairing code shown in the Shield app (AI spending allowance \u2192 Bind your own agent): pass it as code. (2) The user's email: pass email, and Shield emails them a 6-digit code; ask the user for it, then call again with email and emailCode. Ask the user; never guess a code. After pairing, shield.request_purchase and the other shield.* tools ask the user's own spending rules.",
8488
+ inputSchema: {
8489
+ type: "object",
8490
+ properties: {
8491
+ code: {
8492
+ type: "string",
8493
+ pattern: "^[A-Za-z0-9]{6,8}$",
8494
+ description: "The pairing code from the Shield app."
8495
+ },
8496
+ email: {
8497
+ type: "string",
8498
+ maxLength: 254,
8499
+ description: "The user's Shield email, to be sent a sign-in code."
8500
+ },
8501
+ emailCode: {
8502
+ type: "string",
8503
+ pattern: "^[0-9]{6}$",
8504
+ description: "The 6-digit code Shield emailed to the user."
8505
+ },
8506
+ displayName: {
8507
+ type: "string",
8508
+ maxLength: 120,
8509
+ description: "How the Shield app should list this assistant (default: the client and this computer)."
8510
+ }
8511
+ },
8512
+ additionalProperties: false
8513
+ },
8514
+ annotations: networkMutation,
8515
+ handler: async (args) => {
8516
+ const displayName = typeof args.displayName === "string" && args.displayName.trim() ? args.displayName.trim().slice(0, 120) : defaultAgentDisplayName(options.clientName?.());
8517
+ const agent = {
8518
+ app_id: SHIELD_MCP_APP_ID,
8519
+ agent_id: store.agentId(),
8520
+ display_name: displayName
8521
+ };
8522
+ const email = typeof args.email === "string" ? args.email.trim().toLowerCase() : "";
8523
+ const emailCode = typeof args.emailCode === "string" ? args.emailCode.trim() : "";
8524
+ if (email) {
8525
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
8526
+ return {
8527
+ content: text4(
8528
+ "That is not an email address. Ask the user for the email they use with Shield."
8529
+ ),
8530
+ isError: true
8531
+ };
8532
+ }
8533
+ if (!emailCode) {
8534
+ let response3;
8535
+ try {
8536
+ response3 = await post("/v1/consumer/auth/start", { email, scope: "agent" });
8537
+ } catch (error) {
8538
+ return unreachable(error);
8539
+ }
8540
+ const payload = await response3.json().catch(() => null);
8541
+ if (!response3.ok || !payload || typeof payload.challenge_id !== "string") {
8542
+ return {
8543
+ content: text4({
8544
+ error: response3.status === 429 ? "SHIELD_TOO_MANY_CODES" : `HTTP ${response3.status}`,
8545
+ message: response3.status === 429 ? "Shield has sent several codes already. Wait a minute, then try again." : "Shield could not send a code right now. Nothing was paired."
8546
+ }),
8547
+ isError: true
8548
+ };
8549
+ }
8550
+ pendingEmail.set(email, payload.challenge_id);
8551
+ return {
8552
+ content: text4({
8553
+ emailed: true,
8554
+ email,
8555
+ next: `Ask the user for the 6-digit code Shield just emailed to ${email}, then call shield.pair again with email and emailCode.`
8556
+ })
8557
+ };
8558
+ }
8559
+ if (!/^[0-9]{6}$/.test(emailCode)) {
8560
+ return {
8561
+ content: text4(
8562
+ "The emailed code is 6 digits. Ask the user to read it from the Shield email."
8563
+ ),
8564
+ isError: true
8565
+ };
8566
+ }
8567
+ let response2;
8568
+ try {
8569
+ response2 = await post("/v1/consumer/auth/verify", {
8570
+ email,
8571
+ code: emailCode,
8572
+ ...pendingEmail.has(email) ? { challenge_id: pendingEmail.get(email) } : {},
8573
+ scope: "agent",
8574
+ agent
8575
+ });
8576
+ } catch (error) {
8577
+ return unreachable(error);
8578
+ }
8579
+ if (response2.ok) pendingEmail.delete(email);
8580
+ return keep(response2, displayName, agent.agent_id, {
8581
+ INVALID_CODE: "The code is wrong or has expired (five minutes). Ask the user for the newest code, or call shield.pair with email again to send a fresh one.",
8582
+ OTP_TOO_MANY_ATTEMPTS: "Too many wrong codes for that email. Call shield.pair with email again to send a fresh one.",
8583
+ CHALLENGE_NOT_FOUND: "No code is waiting for that email. Call shield.pair with email first."
8584
+ });
8585
+ }
8586
+ const code = typeof args.code === "string" ? args.code.trim().toUpperCase() : "";
8587
+ if (!/^[A-Z0-9]{6,8}$/.test(code)) {
8588
+ return {
8589
+ content: text4(
8590
+ "Give either the pairing code shown in the Shield app (6 to 8 letters and digits) or the user's email. Ask the user; never guess."
8591
+ ),
8592
+ isError: true
8593
+ };
8594
+ }
8595
+ let response;
8596
+ try {
8597
+ response = await post("/v1/consumer/pairing/claim", { code, agent });
8598
+ } catch (error) {
8599
+ return unreachable(error);
8600
+ }
8601
+ return keep(response, displayName, agent.agent_id, {
8602
+ PAIRING_CODE_INVALID: "The code is wrong, expired (ten minutes) or already used. Ask the user for a fresh one from the Shield app."
8603
+ });
8604
+ }
8605
+ };
8606
+ }
8287
8607
  function createShieldTools(shield) {
8288
8608
  const authorize = async (args) => {
8289
8609
  if (typeof args.amount !== "number" || typeof args.currency !== "string" || typeof args.purpose !== "string") {
8290
8610
  return {
8291
- content: text3(
8611
+ content: text4(
8292
8612
  "amount, currency, and purpose are required. Do not execute the transaction."
8293
8613
  ),
8294
8614
  isError: true
@@ -8343,7 +8663,7 @@ function createShieldTools(shield) {
8343
8663
  },
8344
8664
  annotations: networkMutation,
8345
8665
  handler: async (args) => typeof args.decisionId === "string" ? shieldResult(async () => await shield.requestApproval(args.decisionId)) : {
8346
- content: text3("decisionId is required. Do not execute the transaction."),
8666
+ content: text4("decisionId is required. Do not execute the transaction."),
8347
8667
  isError: true
8348
8668
  }
8349
8669
  },
@@ -8359,7 +8679,7 @@ function createShieldTools(shield) {
8359
8679
  },
8360
8680
  annotations: networkRead,
8361
8681
  handler: async (args) => typeof args.decisionId === "string" ? shieldResult(async () => await shield.decision(args.decisionId)) : {
8362
- content: text3("decisionId is required. Do not execute the transaction."),
8682
+ content: text4("decisionId is required. Do not execute the transaction."),
8363
8683
  isError: true
8364
8684
  }
8365
8685
  }
@@ -8367,10 +8687,10 @@ function createShieldTools(shield) {
8367
8687
  }
8368
8688
  async function shieldResult(work) {
8369
8689
  try {
8370
- return { content: text3(await work()) };
8690
+ return { content: text4(await work()) };
8371
8691
  } catch (error) {
8372
8692
  return {
8373
- content: text3({
8693
+ content: text4({
8374
8694
  error: error instanceof Error ? error.name : "ShieldError",
8375
8695
  message: error instanceof Error ? error.message : String(error),
8376
8696
  safeToExecute: false
@@ -8387,16 +8707,15 @@ if (presenceKey && !process.env.DECIONIS_PRESENCE_SERVICE_TOKEN) {
8387
8707
  }
8388
8708
  var presenceConfigured = presenceKey.length > 0;
8389
8709
  var gateway = new LocalDecionisGateway();
8390
- var shieldConfigured = Boolean(
8391
- process.env.DECIONIS_SHIELD_API_KEY && process.env.DECIONIS_SHIELD_APP_ID
8392
- );
8710
+ var shieldClient = new ShieldClient();
8393
8711
  var handler = createMcpHandler({
8394
8712
  serverInfo: { name: "decionis-mcp", version: MCP_SERVER_VERSION },
8395
8713
  tools: createDecionisTools(
8396
8714
  gateway,
8397
8715
  [
8398
8716
  ...createKubernetesMcpToolsFromEnvironment(),
8399
- ...shieldConfigured ? createShieldTools(new ShieldClient()) : []
8717
+ createShieldPairTool(),
8718
+ ...createShieldTools(shieldClient)
8400
8719
  ],
8401
8720
  { presenceConfigured }
8402
8721
  )