@aipermission/mcp 0.2.43 → 0.2.44

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
@@ -85,6 +85,20 @@ does this automatically.
85
85
 
86
86
  ## Tools
87
87
 
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
91
+ operation failed; do not retry mutations with a new idempotency key blindly.
92
+ If a POST response is lost or incomplete, the bridge returns
93
+ `status: outcome_unknown` and `code: gateway_transport_outcome_unknown`.
94
+ Action calls also return the original `idempotency_key`; no request ID is
95
+ invented. Reconcile using that same key and unchanged input, not a new logical
96
+ operation. Known gateway errors retain their original status and request ID.
97
+ Local input-validation failures do not imply that execution occurred.
98
+ Malformed or incomplete JSON responses are not treated as success. Invalid
99
+ local headers are rejected before dispatch without echoing the API token;
100
+ transport errors never echo raw response fragments or header values.
101
+
88
102
  - `list_connector_targets`
89
103
  - `get_connector_help`
90
104
  - `get_connector_actions`
package/dist/results.js CHANGED
@@ -16,6 +16,8 @@ export function errorResult(error) {
16
16
  const status = error instanceof Error && typeof error.resultStatus === "string" ? error.resultStatus : "error";
17
17
  const requestID = error instanceof Error && Number.isSafeInteger(error.requestID) && error.requestID > 0 ? error.requestID : null;
18
18
  const assistantHint = error instanceof Error && typeof error.assistantHint === "string" ? error.assistantHint : "";
19
+ const idempotencyKey =
20
+ error instanceof Error && typeof error.idempotencyKey === "string" && error.idempotencyKey.length <= 128 ? error.idempotencyKey : "";
19
21
  const retryAfterSeconds =
20
22
  error instanceof Error && Number.isSafeInteger(error.retryAfterSeconds) && error.retryAfterSeconds >= 0
21
23
  ? error.retryAfterSeconds
@@ -31,6 +33,7 @@ export function errorResult(error) {
31
33
  ...(code ? { code } : {}),
32
34
  ...(requestID ? { request_id: requestID } : {}),
33
35
  ...(assistantHint ? { assistant_hint: assistantHint } : {}),
36
+ ...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
34
37
  ...(retryAfterSeconds !== null ? { retry_after_seconds: retryAfterSeconds } : {}),
35
38
  error: message,
36
39
  },
package/dist/server.js CHANGED
@@ -163,60 +163,104 @@ async function apiGet(path) {
163
163
  }
164
164
 
165
165
  async function apiPost(path, body) {
166
- return apiRequest(path, {
167
- method: "POST",
168
- headers: {
169
- "Content-Type": "application/json",
166
+ return apiRequest(
167
+ path,
168
+ {
169
+ method: "POST",
170
+ headers: {
171
+ "Content-Type": "application/json",
172
+ },
173
+ body: JSON.stringify(body),
170
174
  },
171
- body: JSON.stringify(body),
172
- });
173
- }
174
-
175
- async function apiRequest(path, options) {
176
- const response = await apiFetch(path, options);
177
- const text = await response.text();
178
- const data = parseResponseBody(text);
179
- if (!response.ok) {
180
- throw gatewayAPIError(data, response.status);
181
- }
182
- return data;
175
+ body?.idempotency_key,
176
+ );
183
177
  }
184
178
 
185
- async function apiFetch(path, options) {
179
+ async function apiRequest(path, options, idempotencyKey) {
186
180
  if (!apiToken) {
187
181
  throw new Error("AIPERMISSION_API_TOKEN is required.");
188
182
  }
183
+ let request;
184
+ try {
185
+ request = new Request(`${apiUrl}${path}`, {
186
+ ...options,
187
+ headers: new Headers({ Authorization: `Bearer ${apiToken}`, ...(options.headers || {}) }),
188
+ });
189
+ } catch {
190
+ throw new Error("Invalid local gateway request configuration; check the API URL and token.");
191
+ }
189
192
  const timeout = Number.isFinite(apiTimeoutMs) && apiTimeoutMs > 0 ? apiTimeoutMs : 60000;
190
193
  const controller = new AbortController();
191
194
  const timer = setTimeout(() => controller.abort(), timeout);
192
- let response;
195
+ let bodyReceived = false;
193
196
  try {
194
- response = await fetch(`${apiUrl}${path}`, {
195
- ...options,
196
- signal: controller.signal,
197
- headers: {
198
- Authorization: `Bearer ${apiToken}`,
199
- ...(options.headers || {}),
200
- },
201
- });
197
+ const response = await fetch(request, { signal: controller.signal });
198
+ const text = await response.text();
199
+ const data = response.status === 204 ? null : parseResponseBody(text);
200
+ bodyReceived = true;
201
+ if (!response.ok) {
202
+ throw gatewayAPIError(data, response.status);
203
+ }
204
+ return data;
202
205
  } catch (error) {
203
- if (error?.name === "AbortError") {
204
- throw new Error(`AIPermission API request timed out after ${timeout}ms`, { cause: error });
206
+ let failure = bodyReceived ? error : new Error("Gateway response unavailable or invalid.", { cause: error });
207
+ if (controller.signal.aborted) {
208
+ failure = new Error(`AIPermission API request timed out after ${timeout}ms`, { cause: error });
209
+ }
210
+ const definitelyNotDispatched = !bodyReceived && isDefinitePredispatchTransportError(error);
211
+ if (definitelyNotDispatched) {
212
+ throw new Error("AIPermission gateway connection failed before request dispatch.", { cause: error });
205
213
  }
206
- throw error;
214
+ if (options.method === "POST" && !bodyReceived) {
215
+ const uncertain = new Error(failure.message || "Gateway response unavailable", { cause: failure });
216
+ uncertain.resultStatus = "outcome_unknown";
217
+ uncertain.code = "gateway_transport_outcome_unknown";
218
+ uncertain.idempotencyKey = idempotencyKey;
219
+ uncertain.assistantHint = idempotencyKey
220
+ ? "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."
221
+ : "The gateway response was lost; execution may have occurred. Inspect the original request status before repeating the operation.";
222
+ throw uncertain;
223
+ }
224
+ throw failure;
207
225
  } finally {
208
226
  clearTimeout(timer);
209
227
  }
210
- return response;
211
228
  }
212
229
 
213
- function parseResponseBody(text) {
214
- if (!text) {
215
- return null;
230
+ const definitePredispatchErrorCodes = new Set([
231
+ "ECONNREFUSED",
232
+ "ENETUNREACH",
233
+ "EHOSTUNREACH",
234
+ "ENOTFOUND",
235
+ "EAI_AGAIN",
236
+ "UND_ERR_CONNECT_TIMEOUT",
237
+ "ERR_TLS_CERT_ALTNAME_INVALID",
238
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
239
+ "SELF_SIGNED_CERT_IN_CHAIN",
240
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
241
+ "CERT_HAS_EXPIRED",
242
+ ]);
243
+
244
+ function isDefinitePredispatchTransportError(error) {
245
+ const pending = [error];
246
+ const visited = new Set();
247
+ while (pending.length > 0) {
248
+ const current = pending.pop();
249
+ if (!current || (typeof current !== "object" && typeof current !== "function") || visited.has(current)) continue;
250
+ visited.add(current);
251
+ if (definitePredispatchErrorCodes.has(current.code)) return true;
252
+ if (current.cause) pending.push(current.cause);
253
+ if (Array.isArray(current.errors)) pending.push(...current.errors);
216
254
  }
255
+ return false;
256
+ }
257
+
258
+ function parseResponseBody(text) {
217
259
  try {
218
- return JSON.parse(text);
260
+ const data = JSON.parse(text);
261
+ if (!data || typeof data !== "object") throw new Error("Invalid response shape");
262
+ return data;
219
263
  } catch {
220
- return { error: text };
264
+ throw new Error("Gateway returned an invalid JSON response.");
221
265
  }
222
266
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aipermission/mcp",
3
- "version": "0.2.43",
3
+ "version": "0.2.44",
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",
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.43",
6
+ "version": "0.2.44",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "@aipermission/mcp",
11
- "version": "0.2.43",
11
+ "version": "0.2.44",
12
12
  "transport": {
13
13
  "type": "stdio"
14
14
  }