@aipermission/mcp 0.2.51 → 0.2.53

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
@@ -86,8 +86,10 @@ does this automatically.
86
86
  ## Tools
87
87
 
88
88
  `AIPERMISSION_HTTP_TIMEOUT_MS` sets the gateway request deadline (default:
89
- 60000 milliseconds). It covers both response headers and the complete response
90
- body, including streamed bodies. A timeout does not prove that a submitted
89
+ 60000 milliseconds; maximum: 600000). The value must be a positive base-10
90
+ integer; invalid configuration stops the bridge instead of silently changing
91
+ the deadline. It covers both response headers and the complete response body,
92
+ including streamed bodies. A timeout does not prove that a submitted
91
93
  operation failed; do not retry mutations with a new idempotency key blindly.
92
94
  If a POST response is lost or incomplete, the bridge returns
93
95
  `status: outcome_unknown` and `code: gateway_transport_outcome_unknown`.
@@ -103,6 +105,12 @@ and `Retry-After`. Respect that delay; immediate retry loops cannot bypass the
103
105
  gateway's per-workspace/token rate, persisted-running concurrency, input, or
104
106
  atomic projected-storage limits.
105
107
 
108
+ The normal `npm test` command requires a full Git clone so its test-manifest
109
+ ratchet can compare against trusted history. A GitHub source archive has no
110
+ history; use `npm run test:source-archive` there to run the complete current
111
+ behavior suite and current-manifest checks. That archive command intentionally
112
+ does not claim to verify the historical anti-regression ratchet.
113
+
106
114
  - `list_connector_targets`
107
115
  - `get_connector_help`
108
116
  - `get_connector_actions`
package/dist/api-error.js CHANGED
@@ -1,13 +1,20 @@
1
1
  const safeResultStatuses = new Set(["failed", "blocked", "stale", "declined", "canceled", "error", "outcome_unknown"]);
2
2
 
3
- export function gatewayAPIError(data, httpStatus) {
3
+ export function gatewayAPIError(data, httpStatus, retryAfterHeader = null) {
4
4
  const error = new Error(data?.error || `AIPermission API request failed with ${httpStatus}`);
5
5
  if (typeof data?.code === "string" && data.code.length <= 128) error.code = data.code;
6
6
  if (safeResultStatuses.has(data?.status)) error.resultStatus = data.status;
7
7
  if (Number.isSafeInteger(data?.request_id) && data.request_id > 0) error.requestID = data.request_id;
8
8
  if (typeof data?.assistant_hint === "string" && data.assistant_hint.length <= 2048) error.assistantHint = data.assistant_hint;
9
- if (Number.isSafeInteger(data?.retry_after_seconds) && data.retry_after_seconds >= 0 && data.retry_after_seconds <= 3600) {
10
- error.retryAfterSeconds = data.retry_after_seconds;
11
- }
9
+ const retryAfterSeconds = safeRetryAfterSeconds(data?.retry_after_seconds, retryAfterHeader);
10
+ if (retryAfterSeconds !== null) error.retryAfterSeconds = retryAfterSeconds;
12
11
  return error;
13
12
  }
13
+
14
+ function safeRetryAfterSeconds(bodyValue, headerValue) {
15
+ if (Number.isSafeInteger(bodyValue) && bodyValue >= 0 && bodyValue <= 3600) return bodyValue;
16
+ const raw = typeof headerValue === "string" ? headerValue.trim() : "";
17
+ if (!/^\d{1,4}$/.test(raw)) return null;
18
+ const value = Number(raw);
19
+ return value <= 3600 ? value : null;
20
+ }
package/dist/config.js ADDED
@@ -0,0 +1,15 @@
1
+ const defaultHTTPTimeoutMs = 60_000;
2
+ const maxHTTPTimeoutMs = 10 * 60_000;
3
+
4
+ export function parseHTTPTimeout(value) {
5
+ const raw = String(value ?? "").trim();
6
+ if (!raw) return defaultHTTPTimeoutMs;
7
+ if (!/^[1-9]\d*$/.test(raw)) {
8
+ throw new Error("AIPERMISSION_HTTP_TIMEOUT_MS must be a positive integer in milliseconds.");
9
+ }
10
+ const timeout = Number(raw);
11
+ if (!Number.isSafeInteger(timeout) || timeout > maxHTTPTimeoutMs) {
12
+ throw new Error(`AIPERMISSION_HTTP_TIMEOUT_MS must not exceed ${maxHTTPTimeoutMs} milliseconds.`);
13
+ }
14
+ return timeout;
15
+ }
@@ -0,0 +1,8 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { z } from "zod";
3
+
4
+ export const idempotencyKeySchema = z
5
+ .string()
6
+ .min(1)
7
+ .refine((value) => Buffer.byteLength(value, "utf8") <= 128, "Idempotency key must not exceed 128 UTF-8 bytes.")
8
+ .describe("Caller-stable key used to prevent duplicate action requests.");
package/dist/server.js CHANGED
@@ -13,6 +13,8 @@ import { z } from "zod";
13
13
  import { callVaultActionSchema, listVaultItemsSchema, vaultActionRequestSchema } from "./vault-tools.js";
14
14
  import { MCP_SERVER_INSTRUCTIONS } from "./instructions.js";
15
15
  import { gatewayAPIError } from "./api-error.js";
16
+ import { parseHTTPTimeout } from "./config.js";
17
+ import { idempotencyKeySchema } from "./idempotency-key.js";
16
18
  import { normalizeLocalAPIURL } from "./local-url.js";
17
19
  import { jsonToolResult } from "./results.js";
18
20
 
@@ -25,7 +27,7 @@ try {
25
27
  apiURLConfigurationError = new Error("Invalid local gateway URL configuration; update AIPERMISSION_API_URL.", { cause: error });
26
28
  }
27
29
  const apiToken = process.env.AIPERMISSION_API_TOKEN || "";
28
- const apiTimeoutMs = Number.parseInt(process.env.AIPERMISSION_HTTP_TIMEOUT_MS || "60000", 10);
30
+ const apiTimeoutMs = parseHTTPTimeout(process.env.AIPERMISSION_HTTP_TIMEOUT_MS);
29
31
 
30
32
  const server = new McpServer(
31
33
  {
@@ -80,11 +82,7 @@ server.tool(
80
82
  action_name: z.string().min(1).describe("Action name from get_connector_actions."),
81
83
  input: z.record(z.unknown()).optional().describe("Connector-specific action input."),
82
84
  reason: z.string().optional().describe("Why this connector action is needed."),
83
- idempotency_key: z
84
- .string()
85
- .min(1)
86
- .max(128)
87
- .describe("Caller-stable key that makes retries return the original request without running twice."),
85
+ idempotency_key: idempotencyKeySchema,
88
86
  },
89
87
  async ({ target_ref, action_name, input, reason, idempotency_key }) => {
90
88
  return jsonToolResult(() =>
@@ -155,7 +153,7 @@ server.tool(
155
153
  "Cancel one approval_pending Vault action request owned by this token. Running or terminal requests cannot be canceled.",
156
154
  vaultActionRequestSchema,
157
155
  async ({ request_id }) => {
158
- return jsonToolResult(() => apiPost(`/api/mcp/vault-action-requests/${request_id}/cancel`, {}));
156
+ return jsonToolResult(() => apiPost(`/api/mcp/vault-action-requests/${request_id}/cancel`, {}, { requestID: request_id }));
159
157
  },
160
158
  );
161
159
 
@@ -168,7 +166,7 @@ async function apiGet(path) {
168
166
  });
169
167
  }
170
168
 
171
- async function apiPost(path, body) {
169
+ async function apiPost(path, body, context = {}) {
172
170
  return apiRequest(
173
171
  path,
174
172
  {
@@ -179,10 +177,11 @@ async function apiPost(path, body) {
179
177
  body: JSON.stringify(body),
180
178
  },
181
179
  body?.idempotency_key,
180
+ context,
182
181
  );
183
182
  }
184
183
 
185
- async function apiRequest(path, options, idempotencyKey) {
184
+ async function apiRequest(path, options, idempotencyKey, context = {}) {
186
185
  if (apiURLConfigurationError) {
187
186
  throw apiURLConfigurationError;
188
187
  }
@@ -198,7 +197,7 @@ async function apiRequest(path, options, idempotencyKey) {
198
197
  } catch {
199
198
  throw new Error("Invalid local gateway request configuration; check the API URL and token.");
200
199
  }
201
- const timeout = Number.isFinite(apiTimeoutMs) && apiTimeoutMs > 0 ? apiTimeoutMs : 60000;
200
+ const timeout = apiTimeoutMs;
202
201
  const controller = new AbortController();
203
202
  const timer = setTimeout(() => controller.abort(), timeout);
204
203
  let bodyReceived = false;
@@ -208,7 +207,7 @@ async function apiRequest(path, options, idempotencyKey) {
208
207
  const data = response.status === 204 ? null : parseResponseBody(text);
209
208
  bodyReceived = true;
210
209
  if (!response.ok) {
211
- throw gatewayAPIError(data, response.status);
210
+ throw gatewayAPIError(data, response.status, response.headers.get("Retry-After"));
212
211
  }
213
212
  return data;
214
213
  } catch (error) {
@@ -225,6 +224,7 @@ async function apiRequest(path, options, idempotencyKey) {
225
224
  uncertain.resultStatus = "outcome_unknown";
226
225
  uncertain.code = "gateway_transport_outcome_unknown";
227
226
  uncertain.idempotencyKey = idempotencyKey;
227
+ if (Number.isSafeInteger(context.requestID) && context.requestID > 0) uncertain.requestID = context.requestID;
228
228
  uncertain.assistantHint = idempotencyKey
229
229
  ? "The gateway response was lost; execution may have occurred. Reconcile the original request using the same idempotency key and unchanged input. Never retry with a new key blindly."
230
230
  : "The gateway response was lost; execution may have occurred. Inspect the original request status before repeating the operation.";
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { idempotencyKeySchema } from "./idempotency-key.js";
2
3
 
3
4
  export const listVaultItemsSchema = {
4
5
  project_ref: z.string().min(1).optional().describe("Optional project id or slug. Omit to list every readable project."),
@@ -9,7 +10,7 @@ export const callVaultActionSchema = {
9
10
  action_name: z.enum(["generate_item", "restart_session_with_environment"]),
10
11
  input: z.record(z.unknown()).describe("Action input. Never include raw secret values."),
11
12
  reason: z.string().min(1).describe("Why this Vault action is needed."),
12
- idempotency_key: z.string().min(1).max(128).describe("Caller-stable key used to prevent duplicate action requests."),
13
+ idempotency_key: idempotencyKeySchema,
13
14
  };
14
15
 
15
16
  export const vaultActionRequestSchema = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aipermission/mcp",
3
- "version": "0.2.51",
3
+ "version": "0.2.53",
4
4
  "mcpName": "io.github.aipermission/aipermission-mcp",
5
5
  "description": "Local-first MCP bridge for the aipermission gateway.",
6
6
  "license": "AGPL-3.0-only",
@@ -33,6 +33,7 @@
33
33
  "lint": "eslint src test scripts",
34
34
  "prepack": "npm run build",
35
35
  "test": "npm run build && node scripts/run-tests.js",
36
+ "test:source-archive": "npm run build && node scripts/run-tests.js --current-only",
36
37
  "test:windows-acl": "node scripts/run-windows-acl-tests.js",
37
38
  "start": "node dist/cli.js",
38
39
  "dev": "node src/cli.js"
package/server.json CHANGED
@@ -3,12 +3,12 @@
3
3
  "name": "io.github.aipermission/aipermission-mcp",
4
4
  "title": "AIPermission",
5
5
  "description": "Local-first MCP bridge for the AIPermission gateway.",
6
- "version": "0.2.51",
6
+ "version": "0.2.53",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "@aipermission/mcp",
11
- "version": "0.2.51",
11
+ "version": "0.2.53",
12
12
  "transport": {
13
13
  "type": "stdio"
14
14
  }