@aipermission/mcp 0.2.54 → 0.2.56

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,7 +86,7 @@ does this automatically.
86
86
  ## Tools
87
87
 
88
88
  `AIPERMISSION_HTTP_TIMEOUT_MS` sets the gateway request deadline (default:
89
- 60000 milliseconds; maximum: 600000). The value must be a positive base-10
89
+ 95000 milliseconds; maximum: 600000). The value must be a positive base-10
90
90
  integer; invalid configuration stops the bridge instead of silently changing
91
91
  the deadline. It covers both response headers and the complete response body,
92
92
  including streamed bodies. A timeout does not prove that a submitted
@@ -228,12 +228,18 @@ disclose secrets. Never automatically retry `submission_unknown`.
228
228
 
229
229
  Connector responses can include `approval_pending` or `running`. Poll
230
230
  `get_connector_action_request(request_id)` until the request reaches a terminal
231
- status. `outcome_unknown` is terminal and means the gateway could not prove the
232
- remote outcome after interruption; inspect target state or ask the operator
231
+ status. Failed action calls carry the MCP `isError` flag; reading a recorded
232
+ failed request is a successful read and does not. A `stopped` call response is a
233
+ gateway control result, not a recorded request. `outcome_unknown` is terminal
234
+ and means the gateway could not prove the remote outcome after interruption;
235
+ inspect target state or ask the operator
233
236
  before retrying. Gateway API errors with that status retain their request id,
234
- assistant hint, and bounded retry delay in the MCP error envelope. MCP tool
235
- responses never include file contents, gateway
236
- temporary paths, archive staging paths, or local upload contents.
237
+ assistant hint, and bounded retry delay in the MCP error envelope. File-transfer
238
+ queue and status responses do not include transferred file bytes, gateway
239
+ temporary paths, archive staging paths, or local upload contents. Explicitly
240
+ authorized connector read actions may return bounded content, such as S3
241
+ `download_object` `content_base64` or SSH command output; treat it as sensitive
242
+ target data.
237
243
 
238
244
  ## Operator Skill
239
245
 
package/dist/config.js CHANGED
@@ -1,4 +1,4 @@
1
- const defaultHTTPTimeoutMs = 60_000;
1
+ const defaultHTTPTimeoutMs = 95_000;
2
2
  const maxHTTPTimeoutMs = 10 * 60_000;
3
3
 
4
4
  export function parseHTTPTimeout(value) {
@@ -0,0 +1,27 @@
1
+ // Code generated by backend/cmd/openapi. DO NOT EDIT.
2
+
3
+ export const connectorActionStatuses = Object.freeze([
4
+ "completed",
5
+ "failed",
6
+ "canceled",
7
+ "running",
8
+ "approval_pending",
9
+ "blocked",
10
+ "stale",
11
+ "declined",
12
+ "error",
13
+ "outcome_unknown",
14
+ ]);
15
+
16
+ export const connectorRetryClasses = Object.freeze(["read_only", "idempotent", "conditional", "non_idempotent"]);
17
+
18
+ export const executionRules = Object.freeze(["always_run", "approval_required", "blocked"]);
19
+
20
+ export const connectorActionResponseRequiredFields = Object.freeze([
21
+ "status",
22
+ "request_id",
23
+ "target_ref",
24
+ "connector_kind",
25
+ "action_name",
26
+ "retry_policy",
27
+ ]);
package/dist/init.js CHANGED
@@ -609,29 +609,40 @@ function removeTOMLServer(source, name) {
609
609
  }
610
610
 
611
611
  function scanTOMLHeader(line, state) {
612
- if (state.multiline) {
613
- if (hasMultilineDelimiter(line, state.multiline)) state.multiline = "";
614
- return null;
615
- }
616
612
  const trimmed = line.trimStart();
617
- if (trimmed.startsWith("[")) {
613
+ if (!state.multiline && trimmed.startsWith("[")) {
618
614
  try {
619
615
  return findMarkerPath(parseTOML(`${line}\n__aipermission_header_marker = true\n`));
620
616
  } catch {
621
- return null;
617
+ // A string can start with a bracket; scan it before the next line.
622
618
  }
623
619
  }
624
- for (const delimiter of ['"""', "'''"]) {
625
- if (!hasMultilineDelimiter(line, delimiter)) continue;
626
- if ((line.split(delimiter).length - 1) % 2 === 1) state.multiline = delimiter;
627
- break;
628
- }
620
+ scanTOMLStrings(line, state);
629
621
  return null;
630
622
  }
631
623
 
632
- function hasMultilineDelimiter(line, delimiter) {
633
- const comment = line.indexOf("#");
634
- return (comment < 0 ? line : line.slice(0, comment)).includes(delimiter);
624
+ function scanTOMLStrings(line, state) {
625
+ let quote = state.multiline;
626
+ for (let index = 0; index < line.length; index += 1) {
627
+ const character = line[index];
628
+ if (!quote && character === "#") break;
629
+ if (quote.startsWith('"') && character === "\\") {
630
+ index += 1;
631
+ continue;
632
+ }
633
+ if (quote) {
634
+ if (line.startsWith(quote, index)) {
635
+ index += quote.length - 1;
636
+ quote = "";
637
+ }
638
+ continue;
639
+ }
640
+ if (character !== '"' && character !== "'") continue;
641
+ const triple = character.repeat(3);
642
+ quote = line.startsWith(triple, index) ? triple : character;
643
+ index += quote.length - 1;
644
+ }
645
+ state.multiline = quote.length === 3 ? quote : "";
635
646
  }
636
647
 
637
648
  function findMarkerPath(value, pathParts = []) {
@@ -113,6 +113,7 @@ Terminal statuses:
113
113
  ```text
114
114
  completed
115
115
  failed
116
+ canceled
116
117
  declined
117
118
  blocked
118
119
  error
@@ -193,8 +194,11 @@ files or inspect remote paths. Prefer the smallest explicit path set. Do not use
193
194
  globs, recursive copy, or directory transfer unless a connector action
194
195
  explicitly supports that behavior.
195
196
 
196
- MCP connector responses never include file contents, gateway temp paths, archive
197
- staging paths, or local upload contents.
197
+ File-transfer queue and status responses do not include transferred file bytes,
198
+ gateway temp paths, archive staging paths, or local upload contents. Explicitly
199
+ authorized connector read actions may return bounded content, such as S3
200
+ `download_object` `content_base64` or SSH command output; treat it as sensitive
201
+ target data.
198
202
 
199
203
  ## S3/Object Storage Practice
200
204
 
@@ -259,7 +263,8 @@ the operator how to rotate or redact it.
259
263
  Use Project Vault only through its dedicated tools:
260
264
 
261
265
  1. Call `list_vault_items(project_ref)` to discover names and non-secret
262
- metadata. Never ask the gateway to reveal values.
266
+ metadata. Prefer an explicit `id:<id>` or `slug:<slug>` reference. Never ask
267
+ the gateway to reveal values.
263
268
  2. Use `call_vault_action` with `generate_item` only when the operator asked for
264
269
  a new secret. Use a stable, unique `idempotency_key`. Send `tags` as a string
265
270
  array, `shared_project_ids` as an integer array, and `usage_notes` as an
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { connectorActionStatuses, connectorRetryClasses } from "./generated-connector-contract.js";
2
3
 
3
4
  const positiveID = z.number().int().positive();
4
5
  const nonNegativeInteger = z.number().int().nonnegative();
@@ -97,7 +98,7 @@ const outputHintSchema = z
97
98
  .strict();
98
99
  const retryPolicySchema = z
99
100
  .object({
100
- class: z.string(),
101
+ class: z.enum([...connectorRetryClasses]),
101
102
  precondition_fields: z.array(z.string()).optional(),
102
103
  guidance: z.string(),
103
104
  })
@@ -119,28 +120,39 @@ const actionDefinitionSchema = z
119
120
 
120
121
  const connectorActionsSchema = z.object({ items: z.array(actionDefinitionSchema) }).strict();
121
122
 
122
- const connectorActionResponseSchema = z
123
+ const connectorActionRequestSchema = z
123
124
  .object({
124
- status: z.string(),
125
- request_id: positiveID.optional(),
126
- target_ref: z.string().optional(),
125
+ status: z.enum([...connectorActionStatuses]),
126
+ request_id: positiveID,
127
+ target_ref: z.string(),
127
128
  target_name: z.string().optional(),
128
- connector_kind: z.string().optional(),
129
+ connector_kind: z.string(),
129
130
  profile_label: z.string().optional(),
130
- action_name: z.string().optional(),
131
+ action_name: z.string(),
131
132
  input: z.record(z.unknown()).optional(),
132
133
  // Connector-owned output is intentionally opaque. The gateway credential
133
134
  // boundary redacts values; this schema owns only the shared MCP envelope.
134
135
  output: z.unknown().optional(),
135
136
  display_text: z.string().optional(),
136
137
  error: z.string().optional(),
137
- retry_policy: retryPolicySchema.optional(),
138
- retry_after_seconds: nonNegativeInteger.optional(),
138
+ retry_policy: retryPolicySchema,
139
+ retry_after_seconds: nonNegativeInteger.max(3600).optional(),
139
140
  assistant_hint: z.string().optional(),
140
141
  output_withheld: z.boolean().optional(),
141
142
  replayed: z.boolean().optional(),
142
143
  })
143
- .strict();
144
+ .strict()
145
+ .superRefine((value, context) => {
146
+ if (value.output_withheld) {
147
+ if (value.input !== undefined || value.output !== undefined || value.display_text !== undefined || value.error !== undefined) {
148
+ context.addIssue({ code: z.ZodIssueCode.custom, message: "withheld action output must not contain request or result content" });
149
+ }
150
+ } else if (!value.target_ref || !value.connector_kind || !value.action_name) {
151
+ context.addIssue({ code: z.ZodIssueCode.custom, message: "action response identity is required" });
152
+ }
153
+ });
154
+ const connectorActionStoppedSchema = z.object({ status: z.literal("stopped"), error: z.string().min(1) }).strict();
155
+ const connectorActionCallSchema = z.union([connectorActionRequestSchema, connectorActionStoppedSchema]);
144
156
 
145
157
  const vaultItemSchema = z
146
158
  .object({
@@ -225,40 +237,76 @@ const vaultSessionOutputSchema = z
225
237
  })
226
238
  .strict();
227
239
 
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();
240
+ const vaultActionStatuses = ["approval_pending", "running", "completed", "failed", "declined", "stale", "canceled", "expired"];
241
+ const vaultActionRecordedShape = {
242
+ status: z.enum(vaultActionStatuses),
243
+ request_id: positiveID,
244
+ project_ref: z.string().min(1),
245
+ reason: z.string().optional(),
246
+ created_at: z.string().optional(),
247
+ expires_at: z.string().optional(),
248
+ secret_values_returned: z.literal(false),
249
+ output_withheld: z.literal(true).optional(),
250
+ retry_after_seconds: nonNegativeInteger.max(3600).optional(),
251
+ assistant_hint: z.string().optional(),
252
+ error: z.string().optional(),
253
+ };
254
+ const vaultActionRecordedSchema = z
255
+ .discriminatedUnion("action_name", [
256
+ z
257
+ .object({
258
+ ...vaultActionRecordedShape,
259
+ action_name: z.literal("generate_item"),
260
+ input: vaultGenerateInputSchema.optional(),
261
+ output: generatedVaultOutputSchema.optional(),
262
+ })
263
+ .strict(),
264
+ z
265
+ .object({
266
+ ...vaultActionRecordedShape,
267
+ action_name: z.literal("restart_session_with_environment"),
268
+ input: vaultSessionInputSchema.optional(),
269
+ output: vaultSessionOutputSchema.optional(),
270
+ })
271
+ .strict(),
272
+ ])
273
+ .superRefine((value, context) => {
274
+ if (value.output_withheld && value.output !== undefined) {
275
+ context.addIssue({ code: z.ZodIssueCode.custom, message: "withheld Vault output must not contain result content" });
276
+ }
277
+ });
278
+ const vaultActionStoppedSchema = z.object({ status: z.literal("stopped"), error: z.string().min(1) }).strict();
279
+ const vaultActionResponseSchema = z.union([vaultActionRecordedSchema, vaultActionStoppedSchema]);
246
280
 
247
281
  export const responseContracts = Object.freeze({
248
282
  connectorTargets: z.array(connectorTargetSchema),
249
283
  connectorHelp: connectorHelpSchema,
250
284
  connectorActions: connectorActionsSchema,
251
- connectorAction: connectorActionResponseSchema,
285
+ connectorActionCall: connectorActionCallSchema,
286
+ connectorActionRequest: connectorActionRequestSchema,
252
287
  vaultItems: vaultItemsSchema,
253
288
  vaultAction: vaultActionResponseSchema,
254
289
  });
255
290
 
256
- export function projectGatewaySuccess(schema, value) {
291
+ export function projectGatewaySuccess(schema, value, expected = undefined) {
257
292
  const result = schema.safeParse(value);
258
293
  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;
294
+ throw gatewayContractError();
295
+ }
296
+ if (expected && result.data?.status !== "stopped") {
297
+ for (const [field, expectedValue] of Object.entries(expected)) {
298
+ if (expectedValue !== undefined && result.data?.[field] !== expectedValue) {
299
+ throw gatewayContractError();
300
+ }
301
+ }
302
+ } else if (expected && (expected.request_id !== undefined || expected.status !== undefined)) {
303
+ throw gatewayContractError();
262
304
  }
263
305
  return result.data;
264
306
  }
307
+
308
+ function gatewayContractError() {
309
+ const error = new Error("Gateway success response failed MCP contract validation.");
310
+ error.code = "gateway_response_contract_invalid";
311
+ return error;
312
+ }
package/dist/results.js CHANGED
@@ -45,7 +45,7 @@ export function errorResult(error) {
45
45
  };
46
46
  }
47
47
 
48
- export async function jsonToolResult(callback, project, mutationContext = null) {
48
+ export async function jsonToolResult(callback, project, mutationContext = null, isFailure = () => false) {
49
49
  if (typeof project !== "function") {
50
50
  return errorResult(new Error("MCP tool result projector is required."));
51
51
  }
@@ -56,12 +56,23 @@ export async function jsonToolResult(callback, project, mutationContext = null)
56
56
  return errorResult(error);
57
57
  }
58
58
  try {
59
- return textResult(project(value));
59
+ const projected = project(value);
60
+ const result = textResult(projected);
61
+ return isFailure(projected) ? { ...result, isError: true } : result;
60
62
  } catch (error) {
61
63
  return errorResult(mutationContext ? projectionOutcomeUnknown(error, mutationContext) : error);
62
64
  }
63
65
  }
64
66
 
67
+ export function jsonActionToolResult(callback, project, mutationContext) {
68
+ return jsonToolResult(
69
+ callback,
70
+ project,
71
+ mutationContext,
72
+ (value) => !["completed", "approval_pending", "running"].includes(value.status),
73
+ );
74
+ }
75
+
65
76
  function projectionOutcomeUnknown(cause, context) {
66
77
  const error = new Error(
67
78
  "The gateway accepted the mutation, but its response failed MCP contract validation. The operation may have completed.",
package/dist/server.js CHANGED
@@ -17,7 +17,7 @@ import { parseHTTPTimeout } from "./config.js";
17
17
  import { idempotencyKeySchema } from "./idempotency-key.js";
18
18
  import { normalizeLocalAPIURL } from "./local-url.js";
19
19
  import { projectGatewaySuccess, responseContracts } from "./response-contracts.js";
20
- import { jsonToolResult } from "./results.js";
20
+ import { jsonActionToolResult, jsonToolResult } from "./results.js";
21
21
  import { externalActionAnnotations, localMutationAnnotations, localReadAnnotations } from "./tool-annotations.js";
22
22
 
23
23
  const packageMetadata = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
@@ -100,7 +100,7 @@ server.tool(
100
100
  },
101
101
  externalActionAnnotations,
102
102
  async ({ target_ref, action_name, input, reason, idempotency_key }, { signal }) => {
103
- return jsonToolResult(
103
+ return jsonActionToolResult(
104
104
  () =>
105
105
  apiPost(
106
106
  "/api/mcp/connector-actions/call",
@@ -113,7 +113,7 @@ server.tool(
113
113
  },
114
114
  { signal },
115
115
  ),
116
- (value) => projectGatewaySuccess(responseContracts.connectorAction, value),
116
+ (value) => projectGatewaySuccess(responseContracts.connectorActionCall, value, { target_ref, action_name }),
117
117
  { idempotencyKey: idempotency_key },
118
118
  );
119
119
  },
@@ -129,7 +129,7 @@ server.tool(
129
129
  async ({ request_id }, { signal }) => {
130
130
  return jsonToolResult(
131
131
  () => apiGet(`/api/mcp/connector-action-requests/${request_id}`, { signal }),
132
- (value) => projectGatewaySuccess(responseContracts.connectorAction, value),
132
+ (value) => projectGatewaySuccess(responseContracts.connectorActionRequest, value, { request_id }),
133
133
  );
134
134
  },
135
135
  );
@@ -154,11 +154,11 @@ server.tool(
154
154
 
155
155
  server.tool(
156
156
  "call_vault_action",
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.",
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, optional secret_type (defaults to generic_secret), 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.",
158
158
  callVaultActionSchema,
159
159
  externalActionAnnotations,
160
160
  async ({ project_ref, action_name, input, reason, idempotency_key }, { signal }) => {
161
- return jsonToolResult(
161
+ return jsonActionToolResult(
162
162
  () =>
163
163
  apiPost(
164
164
  "/api/mcp/vault-actions/call",
@@ -171,7 +171,7 @@ server.tool(
171
171
  },
172
172
  { signal },
173
173
  ),
174
- (value) => projectGatewaySuccess(responseContracts.vaultAction, value),
174
+ (value) => projectGatewaySuccess(responseContracts.vaultAction, value, { project_ref, action_name }),
175
175
  { idempotencyKey: idempotency_key },
176
176
  );
177
177
  },
@@ -185,7 +185,7 @@ server.tool(
185
185
  async ({ request_id }, { signal }) => {
186
186
  return jsonToolResult(
187
187
  () => apiGet(`/api/mcp/vault-action-requests/${request_id}`, { signal }),
188
- (value) => projectGatewaySuccess(responseContracts.vaultAction, value),
188
+ (value) => projectGatewaySuccess(responseContracts.vaultAction, value, { request_id }),
189
189
  );
190
190
  },
191
191
  );
@@ -198,7 +198,7 @@ server.tool(
198
198
  async ({ request_id }, { signal }) => {
199
199
  return jsonToolResult(
200
200
  () => apiPost(`/api/mcp/vault-action-requests/${request_id}/cancel`, {}, { requestID: request_id, signal }),
201
- (value) => projectGatewaySuccess(responseContracts.vaultAction, value),
201
+ (value) => projectGatewaySuccess(responseContracts.vaultAction, value, { request_id, status: "canceled" }),
202
202
  { requestID: request_id },
203
203
  );
204
204
  },
@@ -2,11 +2,16 @@ import { z } from "zod";
2
2
  import { idempotencyKeySchema } from "./idempotency-key.js";
3
3
 
4
4
  export const listVaultItemsSchema = {
5
- project_ref: z.string().min(1).optional().describe("Optional project id or slug. Omit to list every readable project."),
5
+ project_ref: z
6
+ .string()
7
+ .trim()
8
+ .min(1)
9
+ .optional()
10
+ .describe("Optional project reference. Prefer id:<id> or slug:<slug>; omit to list every readable project."),
6
11
  };
7
12
 
8
13
  export const callVaultActionSchema = {
9
- project_ref: z.string().min(1).describe("Owning project id or slug."),
14
+ project_ref: z.string().trim().min(1).describe("Owning project reference. Prefer id:<id> or slug:<slug>."),
10
15
  action_name: z.enum(["generate_item", "restart_session_with_environment"]),
11
16
  input: z.record(z.unknown()).describe("Action input. Never include raw secret values."),
12
17
  reason: z.string().min(1).describe("Why this Vault action is needed."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aipermission/mcp",
3
- "version": "0.2.54",
3
+ "version": "0.2.56",
4
4
  "mcpName": "io.github.aipermission/aipermission-mcp",
5
5
  "description": "Local-only MCP bridge for the aipermission gateway.",
6
6
  "license": "AGPL-3.0-only",
@@ -51,13 +51,13 @@
51
51
  "dependencies": {
52
52
  "@modelcontextprotocol/sdk": "1.30.0",
53
53
  "smol-toml": "1.8.0",
54
- "yaml": "2.9.0",
54
+ "yaml": "2.9.1",
55
55
  "zod": "3.25.76"
56
56
  },
57
57
  "devDependencies": {
58
58
  "@eslint/js": "^10.0.1",
59
59
  "eslint": "^10.10.0",
60
60
  "globals": "^17.11.0",
61
- "prettier": "^3.9.6"
61
+ "prettier": "^3.9.8"
62
62
  }
63
63
  }
package/server.json CHANGED
@@ -3,12 +3,12 @@
3
3
  "name": "io.github.aipermission/aipermission-mcp",
4
4
  "title": "AIPermission",
5
5
  "description": "Local-only MCP bridge for the AIPermission gateway.",
6
- "version": "0.2.54",
6
+ "version": "0.2.56",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "@aipermission/mcp",
11
- "version": "0.2.54",
11
+ "version": "0.2.56",
12
12
  "transport": {
13
13
  "type": "stdio"
14
14
  }