@aipermission/mcp 0.2.53 → 0.2.54

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
@@ -1,6 +1,6 @@
1
1
  # @aipermission/mcp
2
2
 
3
- Local-first MCP bridge for the AIPermission connector gateway.
3
+ Local-only MCP bridge for the AIPermission connector gateway.
4
4
 
5
5
  AIPermission lets AI coding assistants use scoped connector actions through a
6
6
  local gateway without receiving SSH private keys, database passwords, API
@@ -100,10 +100,12 @@ Local input-validation failures do not imply that execution occurred.
100
100
  Malformed or incomplete JSON responses are not treated as success. Invalid
101
101
  local headers are rejected before dispatch without echoing the API token;
102
102
  transport errors never echo raw response fragments or header values.
103
- Connector-action capacity errors retain HTTP `429`, the gateway error code,
104
- and `Retry-After`. Respect that delay; immediate retry loops cannot bypass the
105
- gateway's per-workspace/token rate, persisted-running concurrency, input, or
106
- atomic projected-storage limits.
103
+ Connector-action capacity errors cross the HTTP transport as MCP error results:
104
+ the gateway error code is retained and `Retry-After` becomes bounded
105
+ `retry_after_seconds` metadata. The numeric HTTP `429` itself is not part of the
106
+ MCP tool result. Respect the returned delay; immediate retry loops cannot bypass
107
+ the gateway's per-workspace/token rate, persisted-running concurrency, input,
108
+ or atomic projected-storage limits.
107
109
 
108
110
  The normal `npm test` command requires a full Git clone so its test-manifest
109
111
  ratchet can compare against trusted history. A GitHub source archive has no
@@ -0,0 +1,264 @@
1
+ import { z } from "zod";
2
+
3
+ const positiveID = z.number().int().positive();
4
+ const nonNegativeInteger = z.number().int().nonnegative();
5
+ const forbiddenMetadataField = /(?:^|_)(?:credential|password|passphrase|private_key|secret|token|api_key|access_key)(?:$|_)/;
6
+
7
+ function normalizeMetadataKey(key) {
8
+ return key
9
+ .trim()
10
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
11
+ .replace(/[^A-Za-z0-9]+/g, "_")
12
+ .toLowerCase();
13
+ }
14
+
15
+ function rejectSecretMetadataKeys(value, context) {
16
+ const pending = [value];
17
+ while (pending.length > 0) {
18
+ const current = pending.pop();
19
+ if (Array.isArray(current)) {
20
+ pending.push(...current);
21
+ continue;
22
+ }
23
+ if (!current || typeof current !== "object") continue;
24
+ for (const [key, item] of Object.entries(current)) {
25
+ const normalized = normalizeMetadataKey(key);
26
+ if (forbiddenMetadataField.test(normalized)) {
27
+ context.addIssue({ code: z.ZodIssueCode.custom, message: "connector metadata contains a forbidden secret field" });
28
+ return;
29
+ }
30
+ pending.push(item);
31
+ }
32
+ }
33
+ }
34
+
35
+ const connectorMetadataSchema = z.record(z.unknown()).superRefine(rejectSecretMetadataKeys);
36
+
37
+ const actionGrantSchema = z
38
+ .object({
39
+ name: z.string(),
40
+ execution_rule: z.string(),
41
+ expires_at: z.string().optional(),
42
+ })
43
+ .strict();
44
+
45
+ const connectorTargetSchema = z
46
+ .object({
47
+ target_ref: z.string(),
48
+ project_id: positiveID,
49
+ project_name: z.string(),
50
+ project_slug: z.string(),
51
+ target_id: positiveID,
52
+ target_name: z.string(),
53
+ connector_kind: z.string(),
54
+ profile_id: positiveID,
55
+ profile_label: z.string(),
56
+ profile_kind: z.string(),
57
+ metadata: connectorMetadataSchema.optional(),
58
+ actions: z.array(actionGrantSchema),
59
+ hints: z.array(z.string()).optional(),
60
+ })
61
+ .strict();
62
+
63
+ const connectorHelpSchema = z
64
+ .object({
65
+ title: z.string(),
66
+ summary: z.string(),
67
+ usage: z.array(z.string()).optional(),
68
+ warnings: z.array(z.string()).optional(),
69
+ connector: z.string(),
70
+ connector_id: z.string(),
71
+ })
72
+ .strict();
73
+
74
+ const fieldOptionSchema = z.object({ value: z.string(), label: z.string() }).strict();
75
+ const fieldSchema = z
76
+ .object({
77
+ name: z.string(),
78
+ label: z.string(),
79
+ type: z.string(),
80
+ required: z.boolean().optional(),
81
+ preserve_whitespace: z.boolean().optional(),
82
+ secret: z.boolean().optional(),
83
+ description: z.string().optional(),
84
+ default: z.unknown().optional(),
85
+ options: z.array(fieldOptionSchema).optional(),
86
+ })
87
+ .strict();
88
+ const inputSchema = z.object({ fields: z.array(fieldSchema) }).strict();
89
+ const outputHintSchema = z
90
+ .object({
91
+ format: z.string().optional(),
92
+ sensitive_fields: z.array(z.string()).optional(),
93
+ temporary_capability_fields: z.array(z.string()).optional(),
94
+ max_rows: nonNegativeInteger.optional(),
95
+ max_bytes: nonNegativeInteger.optional(),
96
+ })
97
+ .strict();
98
+ const retryPolicySchema = z
99
+ .object({
100
+ class: z.string(),
101
+ precondition_fields: z.array(z.string()).optional(),
102
+ guidance: z.string(),
103
+ })
104
+ .strict();
105
+ const actionDefinitionSchema = z
106
+ .object({
107
+ name: z.string(),
108
+ label: z.string(),
109
+ description: z.string(),
110
+ category: z.string().optional(),
111
+ risk: z.string(),
112
+ input_schema: inputSchema,
113
+ sensitive_input_fields: z.array(z.string()).optional(),
114
+ output_hint: outputHintSchema.optional(),
115
+ retry_policy: retryPolicySchema,
116
+ max_input_bytes: positiveID,
117
+ })
118
+ .strict();
119
+
120
+ const connectorActionsSchema = z.object({ items: z.array(actionDefinitionSchema) }).strict();
121
+
122
+ const connectorActionResponseSchema = z
123
+ .object({
124
+ status: z.string(),
125
+ request_id: positiveID.optional(),
126
+ target_ref: z.string().optional(),
127
+ target_name: z.string().optional(),
128
+ connector_kind: z.string().optional(),
129
+ profile_label: z.string().optional(),
130
+ action_name: z.string().optional(),
131
+ input: z.record(z.unknown()).optional(),
132
+ // Connector-owned output is intentionally opaque. The gateway credential
133
+ // boundary redacts values; this schema owns only the shared MCP envelope.
134
+ output: z.unknown().optional(),
135
+ display_text: z.string().optional(),
136
+ error: z.string().optional(),
137
+ retry_policy: retryPolicySchema.optional(),
138
+ retry_after_seconds: nonNegativeInteger.optional(),
139
+ assistant_hint: z.string().optional(),
140
+ output_withheld: z.boolean().optional(),
141
+ replayed: z.boolean().optional(),
142
+ })
143
+ .strict();
144
+
145
+ const vaultItemSchema = z
146
+ .object({
147
+ vault_ref: z.string(),
148
+ item_id: positiveID,
149
+ project_ref: z.string(),
150
+ source_project_id: positiveID,
151
+ name: z.string(),
152
+ secret_type: z.string(),
153
+ status: z.string(),
154
+ expires_at: z.string().optional(),
155
+ value_version: positiveID,
156
+ metadata_revision: positiveID,
157
+ })
158
+ .strict();
159
+ const vaultItemsSchema = z
160
+ .object({
161
+ items: z.array(vaultItemSchema),
162
+ count: nonNegativeInteger,
163
+ truncated: z.boolean(),
164
+ secret_values_returned: z.literal(false),
165
+ })
166
+ .strict();
167
+
168
+ const usageNoteSchema = z.object({ location: z.string(), notes: z.string().optional() }).strict();
169
+ const vaultGenerateInputSchema = z
170
+ .object({
171
+ name: z.string(),
172
+ secret_type: z.string().optional(),
173
+ generator_kind: z.string(),
174
+ provider: z.string().optional(),
175
+ environment: z.string().optional(),
176
+ description: z.string().optional(),
177
+ expires_at: z.string().optional(),
178
+ expiry_warning_days: nonNegativeInteger.optional(),
179
+ tags: z.array(z.string()).optional(),
180
+ usage_notes: z.array(usageNoteSchema).optional(),
181
+ shared_project_ids: z.array(positiveID).optional(),
182
+ })
183
+ .strict();
184
+ const vaultSessionItemSchema = z
185
+ .object({
186
+ item_id: positiveID,
187
+ source_project_id: positiveID,
188
+ replace_existing: z.boolean().optional(),
189
+ })
190
+ .strict();
191
+ const vaultSessionInputSchema = z
192
+ .object({
193
+ target_ref: z.string(),
194
+ items: z.array(vaultSessionItemSchema),
195
+ })
196
+ .strict();
197
+
198
+ const generatedVaultItemSchema = z
199
+ .object({
200
+ vault_ref: z.string(),
201
+ item_id: positiveID,
202
+ project_id: positiveID,
203
+ name: z.string(),
204
+ secret_type: z.string(),
205
+ status: z.string(),
206
+ expires_at: z.string(),
207
+ value_version: positiveID,
208
+ metadata_revision: positiveID,
209
+ })
210
+ .strict();
211
+ const generatedVaultOutputSchema = z
212
+ .object({
213
+ item: generatedVaultItemSchema,
214
+ secret_returned: z.literal(false),
215
+ })
216
+ .strict();
217
+ const vaultSessionOutputSchema = z
218
+ .object({
219
+ session_id: positiveID,
220
+ session_generation: positiveID,
221
+ runtime_id: positiveID,
222
+ status: z.string(),
223
+ environment_names: z.array(z.string()),
224
+ expires_at: z.string(),
225
+ })
226
+ .strict();
227
+
228
+ const vaultActionResponseSchema = z
229
+ .object({
230
+ status: z.string(),
231
+ request_id: positiveID.optional(),
232
+ project_ref: z.string().optional(),
233
+ action_name: z.string().optional(),
234
+ input: z.union([vaultGenerateInputSchema, vaultSessionInputSchema]).optional(),
235
+ reason: z.string().optional(),
236
+ created_at: z.string().optional(),
237
+ expires_at: z.string().optional(),
238
+ secret_values_returned: z.literal(false).optional(),
239
+ output: z.union([generatedVaultOutputSchema, vaultSessionOutputSchema]).optional(),
240
+ output_withheld: z.boolean().optional(),
241
+ retry_after_seconds: nonNegativeInteger.optional(),
242
+ assistant_hint: z.string().optional(),
243
+ error: z.string().optional(),
244
+ })
245
+ .strict();
246
+
247
+ export const responseContracts = Object.freeze({
248
+ connectorTargets: z.array(connectorTargetSchema),
249
+ connectorHelp: connectorHelpSchema,
250
+ connectorActions: connectorActionsSchema,
251
+ connectorAction: connectorActionResponseSchema,
252
+ vaultItems: vaultItemsSchema,
253
+ vaultAction: vaultActionResponseSchema,
254
+ });
255
+
256
+ export function projectGatewaySuccess(schema, value) {
257
+ const result = schema.safeParse(value);
258
+ if (!result.success) {
259
+ const error = new Error("Gateway success response failed MCP contract validation.");
260
+ error.code = "gateway_response_contract_invalid";
261
+ throw error;
262
+ }
263
+ return result.data;
264
+ }
package/dist/results.js CHANGED
@@ -45,10 +45,34 @@ export function errorResult(error) {
45
45
  };
46
46
  }
47
47
 
48
- export async function jsonToolResult(callback) {
48
+ export async function jsonToolResult(callback, project, mutationContext = null) {
49
+ if (typeof project !== "function") {
50
+ return errorResult(new Error("MCP tool result projector is required."));
51
+ }
52
+ let value;
49
53
  try {
50
- return textResult(await callback());
54
+ value = await callback();
51
55
  } catch (error) {
52
56
  return errorResult(error);
53
57
  }
58
+ try {
59
+ return textResult(project(value));
60
+ } catch (error) {
61
+ return errorResult(mutationContext ? projectionOutcomeUnknown(error, mutationContext) : error);
62
+ }
63
+ }
64
+
65
+ function projectionOutcomeUnknown(cause, context) {
66
+ const error = new Error(
67
+ "The gateway accepted the mutation, but its response failed MCP contract validation. The operation may have completed.",
68
+ { cause },
69
+ );
70
+ error.resultStatus = "outcome_unknown";
71
+ error.code = "gateway_response_contract_outcome_unknown";
72
+ if (typeof context.idempotencyKey === "string") error.idempotencyKey = context.idempotencyKey;
73
+ if (Number.isSafeInteger(context.requestID) && context.requestID > 0) error.requestID = context.requestID;
74
+ error.assistantHint = context.idempotencyKey
75
+ ? "Reconcile the original request using the same idempotency key and unchanged input. Never retry with a new key blindly."
76
+ : "Inspect the original request status before repeating the operation.";
77
+ return error;
54
78
  }
package/dist/server.js CHANGED
@@ -16,7 +16,9 @@ import { gatewayAPIError } from "./api-error.js";
16
16
  import { parseHTTPTimeout } from "./config.js";
17
17
  import { idempotencyKeySchema } from "./idempotency-key.js";
18
18
  import { normalizeLocalAPIURL } from "./local-url.js";
19
+ import { projectGatewaySuccess, responseContracts } from "./response-contracts.js";
19
20
  import { jsonToolResult } from "./results.js";
21
+ import { externalActionAnnotations, localMutationAnnotations, localReadAnnotations } from "./tool-annotations.js";
20
22
 
21
23
  const packageMetadata = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
22
24
  let apiUrl = "";
@@ -41,8 +43,12 @@ server.tool(
41
43
  "list_connector_targets",
42
44
  "List connector targets this AIPermission token can access. Credentials and secrets are never returned.",
43
45
  {},
44
- async () => {
45
- return jsonToolResult(() => apiGet("/api/mcp/connector-targets"));
46
+ localReadAnnotations,
47
+ async (_args, { signal }) => {
48
+ return jsonToolResult(
49
+ () => apiGet("/api/mcp/connector-targets", { signal }),
50
+ (value) => projectGatewaySuccess(responseContracts.connectorTargets, value),
51
+ );
46
52
  },
47
53
  );
48
54
 
@@ -52,11 +58,15 @@ server.tool(
52
58
  {
53
59
  target_ref: z.string().min(1).describe("Target ref from list_connector_targets in connector:target_id:profile_id format."),
54
60
  },
55
- async ({ target_ref }) => {
56
- return jsonToolResult(() => {
57
- const params = new URLSearchParams({ target_ref });
58
- return apiGet(`/api/mcp/connector-help?${params.toString()}`);
59
- });
61
+ localReadAnnotations,
62
+ async ({ target_ref }, { signal }) => {
63
+ return jsonToolResult(
64
+ () => {
65
+ const params = new URLSearchParams({ target_ref });
66
+ return apiGet(`/api/mcp/connector-help?${params.toString()}`, { signal });
67
+ },
68
+ (value) => projectGatewaySuccess(responseContracts.connectorHelp, value),
69
+ );
60
70
  },
61
71
  );
62
72
 
@@ -66,11 +76,15 @@ server.tool(
66
76
  {
67
77
  target_ref: z.string().min(1).describe("Target ref from list_connector_targets in connector:target_id:profile_id format."),
68
78
  },
69
- async ({ target_ref }) => {
70
- return jsonToolResult(() => {
71
- const params = new URLSearchParams({ target_ref });
72
- return apiGet(`/api/mcp/connector-actions?${params.toString()}`);
73
- });
79
+ localReadAnnotations,
80
+ async ({ target_ref }, { signal }) => {
81
+ return jsonToolResult(
82
+ () => {
83
+ const params = new URLSearchParams({ target_ref });
84
+ return apiGet(`/api/mcp/connector-actions?${params.toString()}`, { signal });
85
+ },
86
+ (value) => projectGatewaySuccess(responseContracts.connectorActions, value),
87
+ );
74
88
  },
75
89
  );
76
90
 
@@ -84,15 +98,23 @@ server.tool(
84
98
  reason: z.string().optional().describe("Why this connector action is needed."),
85
99
  idempotency_key: idempotencyKeySchema,
86
100
  },
87
- async ({ target_ref, action_name, input, reason, idempotency_key }) => {
88
- return jsonToolResult(() =>
89
- apiPost("/api/mcp/connector-actions/call", {
90
- target_ref,
91
- action_name,
92
- input: input || {},
93
- reason: reason || "",
94
- idempotency_key,
95
- }),
101
+ externalActionAnnotations,
102
+ async ({ target_ref, action_name, input, reason, idempotency_key }, { signal }) => {
103
+ return jsonToolResult(
104
+ () =>
105
+ apiPost(
106
+ "/api/mcp/connector-actions/call",
107
+ {
108
+ target_ref,
109
+ action_name,
110
+ input: input || {},
111
+ reason: reason || "",
112
+ idempotency_key,
113
+ },
114
+ { signal },
115
+ ),
116
+ (value) => projectGatewaySuccess(responseContracts.connectorAction, value),
117
+ { idempotencyKey: idempotency_key },
96
118
  );
97
119
  },
98
120
  );
@@ -103,8 +125,12 @@ server.tool(
103
125
  {
104
126
  request_id: z.number().int().positive().describe("Request id returned by call_connector_action."),
105
127
  },
106
- async ({ request_id }) => {
107
- return jsonToolResult(() => apiGet(`/api/mcp/connector-action-requests/${request_id}`));
128
+ localReadAnnotations,
129
+ async ({ request_id }, { signal }) => {
130
+ return jsonToolResult(
131
+ () => apiGet(`/api/mcp/connector-action-requests/${request_id}`, { signal }),
132
+ (value) => projectGatewaySuccess(responseContracts.connectorAction, value),
133
+ );
108
134
  },
109
135
  );
110
136
 
@@ -112,13 +138,17 @@ server.tool(
112
138
  "list_vault_items",
113
139
  "List secret names and bounded non-secret Vault metadata for projects this token can read. Secret values are never returned.",
114
140
  listVaultItemsSchema,
115
- async ({ project_ref }) => {
116
- return jsonToolResult(() => {
117
- const params = new URLSearchParams();
118
- if (project_ref) params.set("project_ref", project_ref);
119
- const query = params.toString();
120
- return apiGet(`/api/mcp/vault-items${query ? `?${query}` : ""}`);
121
- });
141
+ localReadAnnotations,
142
+ async ({ project_ref }, { signal }) => {
143
+ return jsonToolResult(
144
+ () => {
145
+ const params = new URLSearchParams();
146
+ if (project_ref) params.set("project_ref", project_ref);
147
+ const query = params.toString();
148
+ return apiGet(`/api/mcp/vault-items${query ? `?${query}` : ""}`, { signal });
149
+ },
150
+ (value) => projectGatewaySuccess(responseContracts.vaultItems, value),
151
+ );
122
152
  },
123
153
  );
124
154
 
@@ -126,15 +156,23 @@ server.tool(
126
156
  "call_vault_action",
127
157
  "Run a Vault action under the configured project capability. Prompt waits for local approval; Always executes immediately through the same tracked request path. generate_item input accepts name, secret_type, generator_kind, provider, environment, description, expires_at, expiry_warning_days, tags (string array), usage_notes (array of {location, notes}), and shared_project_ids (integer array). restart_session_with_environment input requires target_ref and items with item_id, source_project_id, and optional replace_existing. Never include raw secret values.",
128
158
  callVaultActionSchema,
129
- async ({ project_ref, action_name, input, reason, idempotency_key }) => {
130
- return jsonToolResult(() =>
131
- apiPost("/api/mcp/vault-actions/call", {
132
- project_ref,
133
- action_name,
134
- input,
135
- reason,
136
- idempotency_key,
137
- }),
159
+ externalActionAnnotations,
160
+ async ({ project_ref, action_name, input, reason, idempotency_key }, { signal }) => {
161
+ return jsonToolResult(
162
+ () =>
163
+ apiPost(
164
+ "/api/mcp/vault-actions/call",
165
+ {
166
+ project_ref,
167
+ action_name,
168
+ input,
169
+ reason,
170
+ idempotency_key,
171
+ },
172
+ { signal },
173
+ ),
174
+ (value) => projectGatewaySuccess(responseContracts.vaultAction, value),
175
+ { idempotencyKey: idempotency_key },
138
176
  );
139
177
  },
140
178
  );
@@ -143,8 +181,12 @@ server.tool(
143
181
  "get_vault_action_request",
144
182
  "Read one Vault action request after call_vault_action returns approval_pending. Responses never include secret values.",
145
183
  vaultActionRequestSchema,
146
- async ({ request_id }) => {
147
- return jsonToolResult(() => apiGet(`/api/mcp/vault-action-requests/${request_id}`));
184
+ localReadAnnotations,
185
+ async ({ request_id }, { signal }) => {
186
+ return jsonToolResult(
187
+ () => apiGet(`/api/mcp/vault-action-requests/${request_id}`, { signal }),
188
+ (value) => projectGatewaySuccess(responseContracts.vaultAction, value),
189
+ );
148
190
  },
149
191
  );
150
192
 
@@ -152,18 +194,28 @@ server.tool(
152
194
  "cancel_vault_action_request",
153
195
  "Cancel one approval_pending Vault action request owned by this token. Running or terminal requests cannot be canceled.",
154
196
  vaultActionRequestSchema,
155
- async ({ request_id }) => {
156
- return jsonToolResult(() => apiPost(`/api/mcp/vault-action-requests/${request_id}/cancel`, {}, { requestID: request_id }));
197
+ localMutationAnnotations,
198
+ async ({ request_id }, { signal }) => {
199
+ return jsonToolResult(
200
+ () => apiPost(`/api/mcp/vault-action-requests/${request_id}/cancel`, {}, { requestID: request_id, signal }),
201
+ (value) => projectGatewaySuccess(responseContracts.vaultAction, value),
202
+ { requestID: request_id },
203
+ );
157
204
  },
158
205
  );
159
206
 
160
207
  const transport = new StdioServerTransport();
161
208
  await server.connect(transport);
162
209
 
163
- async function apiGet(path) {
164
- return apiRequest(path, {
165
- method: "GET",
166
- });
210
+ async function apiGet(path, context = {}) {
211
+ return apiRequest(
212
+ path,
213
+ {
214
+ method: "GET",
215
+ },
216
+ undefined,
217
+ context,
218
+ );
167
219
  }
168
220
 
169
221
  async function apiPost(path, body, context = {}) {
@@ -199,9 +251,20 @@ async function apiRequest(path, options, idempotencyKey, context = {}) {
199
251
  }
200
252
  const timeout = apiTimeoutMs;
201
253
  const controller = new AbortController();
202
- const timer = setTimeout(() => controller.abort(), timeout);
254
+ let timedOut = false;
255
+ const timer = setTimeout(() => {
256
+ timedOut = true;
257
+ controller.abort();
258
+ }, timeout);
259
+ const cancelRequest = () => controller.abort();
260
+ context.signal?.addEventListener("abort", cancelRequest, { once: true });
203
261
  let bodyReceived = false;
262
+ let dispatchStarted = false;
204
263
  try {
264
+ if (context.signal?.aborted) {
265
+ throw new Error("MCP request was canceled.");
266
+ }
267
+ dispatchStarted = true;
205
268
  const response = await fetch(request, { signal: controller.signal });
206
269
  const text = await response.text();
207
270
  const data = response.status === 204 ? null : parseResponseBody(text);
@@ -212,9 +275,12 @@ async function apiRequest(path, options, idempotencyKey, context = {}) {
212
275
  return data;
213
276
  } catch (error) {
214
277
  let failure = bodyReceived ? error : new Error("Gateway response unavailable or invalid.", { cause: error });
215
- if (controller.signal.aborted) {
278
+ if (timedOut) {
216
279
  failure = new Error(`AIPermission API request timed out after ${timeout}ms`, { cause: error });
280
+ } else if (context.signal?.aborted) {
281
+ failure = new Error("MCP request was canceled.", { cause: error });
217
282
  }
283
+ if (!dispatchStarted) throw failure;
218
284
  const definitelyNotDispatched = !bodyReceived && isDefinitePredispatchTransportError(error);
219
285
  if (definitelyNotDispatched) {
220
286
  throw new Error("AIPermission gateway connection failed before request dispatch.", { cause: error });
@@ -233,6 +299,7 @@ async function apiRequest(path, options, idempotencyKey, context = {}) {
233
299
  throw failure;
234
300
  } finally {
235
301
  clearTimeout(timer);
302
+ context.signal?.removeEventListener("abort", cancelRequest);
236
303
  }
237
304
  }
238
305
 
@@ -0,0 +1,20 @@
1
+ export const localReadAnnotations = Object.freeze({
2
+ readOnlyHint: true,
3
+ destructiveHint: false,
4
+ idempotentHint: true,
5
+ openWorldHint: false,
6
+ });
7
+
8
+ export const externalActionAnnotations = Object.freeze({
9
+ readOnlyHint: false,
10
+ destructiveHint: true,
11
+ idempotentHint: false,
12
+ openWorldHint: true,
13
+ });
14
+
15
+ export const localMutationAnnotations = Object.freeze({
16
+ readOnlyHint: false,
17
+ destructiveHint: true,
18
+ idempotentHint: false,
19
+ openWorldHint: false,
20
+ });
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@aipermission/mcp",
3
- "version": "0.2.53",
3
+ "version": "0.2.54",
4
4
  "mcpName": "io.github.aipermission/aipermission-mcp",
5
- "description": "Local-first MCP bridge for the aipermission gateway.",
5
+ "description": "Local-only MCP bridge for the aipermission gateway.",
6
6
  "license": "AGPL-3.0-only",
7
7
  "type": "module",
8
8
  "homepage": "https://github.com/aipermission/aipermission/tree/main/packages/mcp#readme",
package/server.json CHANGED
@@ -2,13 +2,13 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.aipermission/aipermission-mcp",
4
4
  "title": "AIPermission",
5
- "description": "Local-first MCP bridge for the AIPermission gateway.",
6
- "version": "0.2.53",
5
+ "description": "Local-only MCP bridge for the AIPermission gateway.",
6
+ "version": "0.2.54",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "@aipermission/mcp",
11
- "version": "0.2.53",
11
+ "version": "0.2.54",
12
12
  "transport": {
13
13
  "type": "stdio"
14
14
  }