@aipermission/mcp 0.2.52 → 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 +17 -7
- package/dist/api-error.js +11 -4
- package/dist/config.js +15 -0
- package/dist/idempotency-key.js +8 -0
- package/dist/response-contracts.js +264 -0
- package/dist/results.js +26 -2
- package/dist/server.js +126 -59
- package/dist/tool-annotations.js +20 -0
- package/dist/vault-tools.js +2 -1
- package/package.json +3 -2
- package/server.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @aipermission/mcp
|
|
2
2
|
|
|
3
|
-
Local-
|
|
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
|
|
@@ -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).
|
|
90
|
-
|
|
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`.
|
|
@@ -98,10 +100,18 @@ Local input-validation failures do not imply that execution occurred.
|
|
|
98
100
|
Malformed or incomplete JSON responses are not treated as success. Invalid
|
|
99
101
|
local headers are rejected before dispatch without echoing the API token;
|
|
100
102
|
transport errors never echo raw response fragments or header values.
|
|
101
|
-
Connector-action capacity errors
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
109
|
+
|
|
110
|
+
The normal `npm test` command requires a full Git clone so its test-manifest
|
|
111
|
+
ratchet can compare against trusted history. A GitHub source archive has no
|
|
112
|
+
history; use `npm run test:source-archive` there to run the complete current
|
|
113
|
+
behavior suite and current-manifest checks. That archive command intentionally
|
|
114
|
+
does not claim to verify the historical anti-regression ratchet.
|
|
105
115
|
|
|
106
116
|
- `list_connector_targets`
|
|
107
117
|
- `get_connector_help`
|
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
|
-
|
|
10
|
-
|
|
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.");
|
|
@@ -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
|
-
|
|
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
|
@@ -13,8 +13,12 @@ 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";
|
|
19
|
+
import { projectGatewaySuccess, responseContracts } from "./response-contracts.js";
|
|
17
20
|
import { jsonToolResult } from "./results.js";
|
|
21
|
+
import { externalActionAnnotations, localMutationAnnotations, localReadAnnotations } from "./tool-annotations.js";
|
|
18
22
|
|
|
19
23
|
const packageMetadata = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
20
24
|
let apiUrl = "";
|
|
@@ -25,7 +29,7 @@ try {
|
|
|
25
29
|
apiURLConfigurationError = new Error("Invalid local gateway URL configuration; update AIPERMISSION_API_URL.", { cause: error });
|
|
26
30
|
}
|
|
27
31
|
const apiToken = process.env.AIPERMISSION_API_TOKEN || "";
|
|
28
|
-
const apiTimeoutMs =
|
|
32
|
+
const apiTimeoutMs = parseHTTPTimeout(process.env.AIPERMISSION_HTTP_TIMEOUT_MS);
|
|
29
33
|
|
|
30
34
|
const server = new McpServer(
|
|
31
35
|
{
|
|
@@ -39,8 +43,12 @@ server.tool(
|
|
|
39
43
|
"list_connector_targets",
|
|
40
44
|
"List connector targets this AIPermission token can access. Credentials and secrets are never returned.",
|
|
41
45
|
{},
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
localReadAnnotations,
|
|
47
|
+
async (_args, { signal }) => {
|
|
48
|
+
return jsonToolResult(
|
|
49
|
+
() => apiGet("/api/mcp/connector-targets", { signal }),
|
|
50
|
+
(value) => projectGatewaySuccess(responseContracts.connectorTargets, value),
|
|
51
|
+
);
|
|
44
52
|
},
|
|
45
53
|
);
|
|
46
54
|
|
|
@@ -50,11 +58,15 @@ server.tool(
|
|
|
50
58
|
{
|
|
51
59
|
target_ref: z.string().min(1).describe("Target ref from list_connector_targets in connector:target_id:profile_id format."),
|
|
52
60
|
},
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
);
|
|
58
70
|
},
|
|
59
71
|
);
|
|
60
72
|
|
|
@@ -64,11 +76,15 @@ server.tool(
|
|
|
64
76
|
{
|
|
65
77
|
target_ref: z.string().min(1).describe("Target ref from list_connector_targets in connector:target_id:profile_id format."),
|
|
66
78
|
},
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
);
|
|
72
88
|
},
|
|
73
89
|
);
|
|
74
90
|
|
|
@@ -80,21 +96,25 @@ server.tool(
|
|
|
80
96
|
action_name: z.string().min(1).describe("Action name from get_connector_actions."),
|
|
81
97
|
input: z.record(z.unknown()).optional().describe("Connector-specific action input."),
|
|
82
98
|
reason: z.string().optional().describe("Why this connector action is needed."),
|
|
83
|
-
idempotency_key:
|
|
84
|
-
.string()
|
|
85
|
-
.min(1)
|
|
86
|
-
.max(128)
|
|
87
|
-
.describe("Caller-stable key that makes retries return the original request without running twice."),
|
|
99
|
+
idempotency_key: idempotencyKeySchema,
|
|
88
100
|
},
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 },
|
|
98
118
|
);
|
|
99
119
|
},
|
|
100
120
|
);
|
|
@@ -105,8 +125,12 @@ server.tool(
|
|
|
105
125
|
{
|
|
106
126
|
request_id: z.number().int().positive().describe("Request id returned by call_connector_action."),
|
|
107
127
|
},
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
);
|
|
110
134
|
},
|
|
111
135
|
);
|
|
112
136
|
|
|
@@ -114,13 +138,17 @@ server.tool(
|
|
|
114
138
|
"list_vault_items",
|
|
115
139
|
"List secret names and bounded non-secret Vault metadata for projects this token can read. Secret values are never returned.",
|
|
116
140
|
listVaultItemsSchema,
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
);
|
|
124
152
|
},
|
|
125
153
|
);
|
|
126
154
|
|
|
@@ -128,15 +156,23 @@ server.tool(
|
|
|
128
156
|
"call_vault_action",
|
|
129
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.",
|
|
130
158
|
callVaultActionSchema,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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 },
|
|
140
176
|
);
|
|
141
177
|
},
|
|
142
178
|
);
|
|
@@ -145,8 +181,12 @@ server.tool(
|
|
|
145
181
|
"get_vault_action_request",
|
|
146
182
|
"Read one Vault action request after call_vault_action returns approval_pending. Responses never include secret values.",
|
|
147
183
|
vaultActionRequestSchema,
|
|
148
|
-
|
|
149
|
-
|
|
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
|
+
);
|
|
150
190
|
},
|
|
151
191
|
);
|
|
152
192
|
|
|
@@ -154,21 +194,31 @@ server.tool(
|
|
|
154
194
|
"cancel_vault_action_request",
|
|
155
195
|
"Cancel one approval_pending Vault action request owned by this token. Running or terminal requests cannot be canceled.",
|
|
156
196
|
vaultActionRequestSchema,
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
);
|
|
159
204
|
},
|
|
160
205
|
);
|
|
161
206
|
|
|
162
207
|
const transport = new StdioServerTransport();
|
|
163
208
|
await server.connect(transport);
|
|
164
209
|
|
|
165
|
-
async function apiGet(path) {
|
|
166
|
-
return apiRequest(
|
|
167
|
-
|
|
168
|
-
|
|
210
|
+
async function apiGet(path, context = {}) {
|
|
211
|
+
return apiRequest(
|
|
212
|
+
path,
|
|
213
|
+
{
|
|
214
|
+
method: "GET",
|
|
215
|
+
},
|
|
216
|
+
undefined,
|
|
217
|
+
context,
|
|
218
|
+
);
|
|
169
219
|
}
|
|
170
220
|
|
|
171
|
-
async function apiPost(path, body) {
|
|
221
|
+
async function apiPost(path, body, context = {}) {
|
|
172
222
|
return apiRequest(
|
|
173
223
|
path,
|
|
174
224
|
{
|
|
@@ -179,10 +229,11 @@ async function apiPost(path, body) {
|
|
|
179
229
|
body: JSON.stringify(body),
|
|
180
230
|
},
|
|
181
231
|
body?.idempotency_key,
|
|
232
|
+
context,
|
|
182
233
|
);
|
|
183
234
|
}
|
|
184
235
|
|
|
185
|
-
async function apiRequest(path, options, idempotencyKey) {
|
|
236
|
+
async function apiRequest(path, options, idempotencyKey, context = {}) {
|
|
186
237
|
if (apiURLConfigurationError) {
|
|
187
238
|
throw apiURLConfigurationError;
|
|
188
239
|
}
|
|
@@ -198,24 +249,38 @@ async function apiRequest(path, options, idempotencyKey) {
|
|
|
198
249
|
} catch {
|
|
199
250
|
throw new Error("Invalid local gateway request configuration; check the API URL and token.");
|
|
200
251
|
}
|
|
201
|
-
const timeout =
|
|
252
|
+
const timeout = apiTimeoutMs;
|
|
202
253
|
const controller = new AbortController();
|
|
203
|
-
|
|
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 });
|
|
204
261
|
let bodyReceived = false;
|
|
262
|
+
let dispatchStarted = false;
|
|
205
263
|
try {
|
|
264
|
+
if (context.signal?.aborted) {
|
|
265
|
+
throw new Error("MCP request was canceled.");
|
|
266
|
+
}
|
|
267
|
+
dispatchStarted = true;
|
|
206
268
|
const response = await fetch(request, { signal: controller.signal });
|
|
207
269
|
const text = await response.text();
|
|
208
270
|
const data = response.status === 204 ? null : parseResponseBody(text);
|
|
209
271
|
bodyReceived = true;
|
|
210
272
|
if (!response.ok) {
|
|
211
|
-
throw gatewayAPIError(data, response.status);
|
|
273
|
+
throw gatewayAPIError(data, response.status, response.headers.get("Retry-After"));
|
|
212
274
|
}
|
|
213
275
|
return data;
|
|
214
276
|
} catch (error) {
|
|
215
277
|
let failure = bodyReceived ? error : new Error("Gateway response unavailable or invalid.", { cause: error });
|
|
216
|
-
if (
|
|
278
|
+
if (timedOut) {
|
|
217
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 });
|
|
218
282
|
}
|
|
283
|
+
if (!dispatchStarted) throw failure;
|
|
219
284
|
const definitelyNotDispatched = !bodyReceived && isDefinitePredispatchTransportError(error);
|
|
220
285
|
if (definitelyNotDispatched) {
|
|
221
286
|
throw new Error("AIPermission gateway connection failed before request dispatch.", { cause: error });
|
|
@@ -225,6 +290,7 @@ async function apiRequest(path, options, idempotencyKey) {
|
|
|
225
290
|
uncertain.resultStatus = "outcome_unknown";
|
|
226
291
|
uncertain.code = "gateway_transport_outcome_unknown";
|
|
227
292
|
uncertain.idempotencyKey = idempotencyKey;
|
|
293
|
+
if (Number.isSafeInteger(context.requestID) && context.requestID > 0) uncertain.requestID = context.requestID;
|
|
228
294
|
uncertain.assistantHint = idempotencyKey
|
|
229
295
|
? "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
296
|
: "The gateway response was lost; execution may have occurred. Inspect the original request status before repeating the operation.";
|
|
@@ -233,6 +299,7 @@ async function apiRequest(path, options, idempotencyKey) {
|
|
|
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/dist/vault-tools.js
CHANGED
|
@@ -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:
|
|
13
|
+
idempotency_key: idempotencyKeySchema,
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
export const vaultActionRequestSchema = {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aipermission/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.54",
|
|
4
4
|
"mcpName": "io.github.aipermission/aipermission-mcp",
|
|
5
|
-
"description": "Local-
|
|
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",
|
|
@@ -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
|
@@ -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-
|
|
6
|
-
"version": "0.2.
|
|
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.
|
|
11
|
+
"version": "0.2.54",
|
|
12
12
|
"transport": {
|
|
13
13
|
"type": "stdio"
|
|
14
14
|
}
|