@aipermission/mcp 0.2.53 → 0.2.55
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 +13 -8
- package/dist/config.js +1 -1
- package/dist/generated-connector-contract.js +27 -0
- package/dist/init.js +25 -14
- package/dist/response-contracts.js +277 -0
- package/dist/results.js +37 -2
- package/dist/server.js +117 -50
- package/dist/tool-annotations.js +20 -0
- package/package.json +3 -3
- 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,7 +86,7 @@ does this automatically.
|
|
|
86
86
|
## Tools
|
|
87
87
|
|
|
88
88
|
`AIPERMISSION_HTTP_TIMEOUT_MS` sets the gateway request deadline (default:
|
|
89
|
-
|
|
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
|
|
@@ -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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
@@ -226,8 +228,11 @@ disclose secrets. Never automatically retry `submission_unknown`.
|
|
|
226
228
|
|
|
227
229
|
Connector responses can include `approval_pending` or `running`. Poll
|
|
228
230
|
`get_connector_action_request(request_id)` until the request reaches a terminal
|
|
229
|
-
status.
|
|
230
|
-
|
|
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
|
|
231
236
|
before retrying. Gateway API errors with that status retain their request id,
|
|
232
237
|
assistant hint, and bounded retry delay in the MCP error envelope. MCP tool
|
|
233
238
|
responses never include file contents, gateway
|
package/dist/config.js
CHANGED
|
@@ -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
|
-
|
|
617
|
+
// A string can start with a bracket; scan it before the next line.
|
|
622
618
|
}
|
|
623
619
|
}
|
|
624
|
-
|
|
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
|
|
633
|
-
|
|
634
|
-
|
|
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 = []) {
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { connectorActionStatuses, connectorRetryClasses } from "./generated-connector-contract.js";
|
|
3
|
+
|
|
4
|
+
const positiveID = z.number().int().positive();
|
|
5
|
+
const nonNegativeInteger = z.number().int().nonnegative();
|
|
6
|
+
const forbiddenMetadataField = /(?:^|_)(?:credential|password|passphrase|private_key|secret|token|api_key|access_key)(?:$|_)/;
|
|
7
|
+
|
|
8
|
+
function normalizeMetadataKey(key) {
|
|
9
|
+
return key
|
|
10
|
+
.trim()
|
|
11
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
12
|
+
.replace(/[^A-Za-z0-9]+/g, "_")
|
|
13
|
+
.toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function rejectSecretMetadataKeys(value, context) {
|
|
17
|
+
const pending = [value];
|
|
18
|
+
while (pending.length > 0) {
|
|
19
|
+
const current = pending.pop();
|
|
20
|
+
if (Array.isArray(current)) {
|
|
21
|
+
pending.push(...current);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (!current || typeof current !== "object") continue;
|
|
25
|
+
for (const [key, item] of Object.entries(current)) {
|
|
26
|
+
const normalized = normalizeMetadataKey(key);
|
|
27
|
+
if (forbiddenMetadataField.test(normalized)) {
|
|
28
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "connector metadata contains a forbidden secret field" });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
pending.push(item);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const connectorMetadataSchema = z.record(z.unknown()).superRefine(rejectSecretMetadataKeys);
|
|
37
|
+
|
|
38
|
+
const actionGrantSchema = z
|
|
39
|
+
.object({
|
|
40
|
+
name: z.string(),
|
|
41
|
+
execution_rule: z.string(),
|
|
42
|
+
expires_at: z.string().optional(),
|
|
43
|
+
})
|
|
44
|
+
.strict();
|
|
45
|
+
|
|
46
|
+
const connectorTargetSchema = z
|
|
47
|
+
.object({
|
|
48
|
+
target_ref: z.string(),
|
|
49
|
+
project_id: positiveID,
|
|
50
|
+
project_name: z.string(),
|
|
51
|
+
project_slug: z.string(),
|
|
52
|
+
target_id: positiveID,
|
|
53
|
+
target_name: z.string(),
|
|
54
|
+
connector_kind: z.string(),
|
|
55
|
+
profile_id: positiveID,
|
|
56
|
+
profile_label: z.string(),
|
|
57
|
+
profile_kind: z.string(),
|
|
58
|
+
metadata: connectorMetadataSchema.optional(),
|
|
59
|
+
actions: z.array(actionGrantSchema),
|
|
60
|
+
hints: z.array(z.string()).optional(),
|
|
61
|
+
})
|
|
62
|
+
.strict();
|
|
63
|
+
|
|
64
|
+
const connectorHelpSchema = z
|
|
65
|
+
.object({
|
|
66
|
+
title: z.string(),
|
|
67
|
+
summary: z.string(),
|
|
68
|
+
usage: z.array(z.string()).optional(),
|
|
69
|
+
warnings: z.array(z.string()).optional(),
|
|
70
|
+
connector: z.string(),
|
|
71
|
+
connector_id: z.string(),
|
|
72
|
+
})
|
|
73
|
+
.strict();
|
|
74
|
+
|
|
75
|
+
const fieldOptionSchema = z.object({ value: z.string(), label: z.string() }).strict();
|
|
76
|
+
const fieldSchema = z
|
|
77
|
+
.object({
|
|
78
|
+
name: z.string(),
|
|
79
|
+
label: z.string(),
|
|
80
|
+
type: z.string(),
|
|
81
|
+
required: z.boolean().optional(),
|
|
82
|
+
preserve_whitespace: z.boolean().optional(),
|
|
83
|
+
secret: z.boolean().optional(),
|
|
84
|
+
description: z.string().optional(),
|
|
85
|
+
default: z.unknown().optional(),
|
|
86
|
+
options: z.array(fieldOptionSchema).optional(),
|
|
87
|
+
})
|
|
88
|
+
.strict();
|
|
89
|
+
const inputSchema = z.object({ fields: z.array(fieldSchema) }).strict();
|
|
90
|
+
const outputHintSchema = z
|
|
91
|
+
.object({
|
|
92
|
+
format: z.string().optional(),
|
|
93
|
+
sensitive_fields: z.array(z.string()).optional(),
|
|
94
|
+
temporary_capability_fields: z.array(z.string()).optional(),
|
|
95
|
+
max_rows: nonNegativeInteger.optional(),
|
|
96
|
+
max_bytes: nonNegativeInteger.optional(),
|
|
97
|
+
})
|
|
98
|
+
.strict();
|
|
99
|
+
const retryPolicySchema = z
|
|
100
|
+
.object({
|
|
101
|
+
class: z.enum([...connectorRetryClasses]),
|
|
102
|
+
precondition_fields: z.array(z.string()).optional(),
|
|
103
|
+
guidance: z.string(),
|
|
104
|
+
})
|
|
105
|
+
.strict();
|
|
106
|
+
const actionDefinitionSchema = z
|
|
107
|
+
.object({
|
|
108
|
+
name: z.string(),
|
|
109
|
+
label: z.string(),
|
|
110
|
+
description: z.string(),
|
|
111
|
+
category: z.string().optional(),
|
|
112
|
+
risk: z.string(),
|
|
113
|
+
input_schema: inputSchema,
|
|
114
|
+
sensitive_input_fields: z.array(z.string()).optional(),
|
|
115
|
+
output_hint: outputHintSchema.optional(),
|
|
116
|
+
retry_policy: retryPolicySchema,
|
|
117
|
+
max_input_bytes: positiveID,
|
|
118
|
+
})
|
|
119
|
+
.strict();
|
|
120
|
+
|
|
121
|
+
const connectorActionsSchema = z.object({ items: z.array(actionDefinitionSchema) }).strict();
|
|
122
|
+
|
|
123
|
+
const connectorActionRequestSchema = z
|
|
124
|
+
.object({
|
|
125
|
+
status: z.enum([...connectorActionStatuses]),
|
|
126
|
+
request_id: positiveID,
|
|
127
|
+
target_ref: z.string(),
|
|
128
|
+
target_name: z.string().optional(),
|
|
129
|
+
connector_kind: z.string(),
|
|
130
|
+
profile_label: z.string().optional(),
|
|
131
|
+
action_name: z.string(),
|
|
132
|
+
input: z.record(z.unknown()).optional(),
|
|
133
|
+
// Connector-owned output is intentionally opaque. The gateway credential
|
|
134
|
+
// boundary redacts values; this schema owns only the shared MCP envelope.
|
|
135
|
+
output: z.unknown().optional(),
|
|
136
|
+
display_text: z.string().optional(),
|
|
137
|
+
error: z.string().optional(),
|
|
138
|
+
retry_policy: retryPolicySchema,
|
|
139
|
+
retry_after_seconds: nonNegativeInteger.max(3600).optional(),
|
|
140
|
+
assistant_hint: z.string().optional(),
|
|
141
|
+
output_withheld: z.boolean().optional(),
|
|
142
|
+
replayed: z.boolean().optional(),
|
|
143
|
+
})
|
|
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]);
|
|
156
|
+
|
|
157
|
+
const vaultItemSchema = z
|
|
158
|
+
.object({
|
|
159
|
+
vault_ref: z.string(),
|
|
160
|
+
item_id: positiveID,
|
|
161
|
+
project_ref: z.string(),
|
|
162
|
+
source_project_id: positiveID,
|
|
163
|
+
name: z.string(),
|
|
164
|
+
secret_type: z.string(),
|
|
165
|
+
status: z.string(),
|
|
166
|
+
expires_at: z.string().optional(),
|
|
167
|
+
value_version: positiveID,
|
|
168
|
+
metadata_revision: positiveID,
|
|
169
|
+
})
|
|
170
|
+
.strict();
|
|
171
|
+
const vaultItemsSchema = z
|
|
172
|
+
.object({
|
|
173
|
+
items: z.array(vaultItemSchema),
|
|
174
|
+
count: nonNegativeInteger,
|
|
175
|
+
truncated: z.boolean(),
|
|
176
|
+
secret_values_returned: z.literal(false),
|
|
177
|
+
})
|
|
178
|
+
.strict();
|
|
179
|
+
|
|
180
|
+
const usageNoteSchema = z.object({ location: z.string(), notes: z.string().optional() }).strict();
|
|
181
|
+
const vaultGenerateInputSchema = z
|
|
182
|
+
.object({
|
|
183
|
+
name: z.string(),
|
|
184
|
+
secret_type: z.string().optional(),
|
|
185
|
+
generator_kind: z.string(),
|
|
186
|
+
provider: z.string().optional(),
|
|
187
|
+
environment: z.string().optional(),
|
|
188
|
+
description: z.string().optional(),
|
|
189
|
+
expires_at: z.string().optional(),
|
|
190
|
+
expiry_warning_days: nonNegativeInteger.optional(),
|
|
191
|
+
tags: z.array(z.string()).optional(),
|
|
192
|
+
usage_notes: z.array(usageNoteSchema).optional(),
|
|
193
|
+
shared_project_ids: z.array(positiveID).optional(),
|
|
194
|
+
})
|
|
195
|
+
.strict();
|
|
196
|
+
const vaultSessionItemSchema = z
|
|
197
|
+
.object({
|
|
198
|
+
item_id: positiveID,
|
|
199
|
+
source_project_id: positiveID,
|
|
200
|
+
replace_existing: z.boolean().optional(),
|
|
201
|
+
})
|
|
202
|
+
.strict();
|
|
203
|
+
const vaultSessionInputSchema = z
|
|
204
|
+
.object({
|
|
205
|
+
target_ref: z.string(),
|
|
206
|
+
items: z.array(vaultSessionItemSchema),
|
|
207
|
+
})
|
|
208
|
+
.strict();
|
|
209
|
+
|
|
210
|
+
const generatedVaultItemSchema = z
|
|
211
|
+
.object({
|
|
212
|
+
vault_ref: z.string(),
|
|
213
|
+
item_id: positiveID,
|
|
214
|
+
project_id: positiveID,
|
|
215
|
+
name: z.string(),
|
|
216
|
+
secret_type: z.string(),
|
|
217
|
+
status: z.string(),
|
|
218
|
+
expires_at: z.string(),
|
|
219
|
+
value_version: positiveID,
|
|
220
|
+
metadata_revision: positiveID,
|
|
221
|
+
})
|
|
222
|
+
.strict();
|
|
223
|
+
const generatedVaultOutputSchema = z
|
|
224
|
+
.object({
|
|
225
|
+
item: generatedVaultItemSchema,
|
|
226
|
+
secret_returned: z.literal(false),
|
|
227
|
+
})
|
|
228
|
+
.strict();
|
|
229
|
+
const vaultSessionOutputSchema = z
|
|
230
|
+
.object({
|
|
231
|
+
session_id: positiveID,
|
|
232
|
+
session_generation: positiveID,
|
|
233
|
+
runtime_id: positiveID,
|
|
234
|
+
status: z.string(),
|
|
235
|
+
environment_names: z.array(z.string()),
|
|
236
|
+
expires_at: z.string(),
|
|
237
|
+
})
|
|
238
|
+
.strict();
|
|
239
|
+
|
|
240
|
+
const vaultActionResponseSchema = z
|
|
241
|
+
.object({
|
|
242
|
+
status: z.string(),
|
|
243
|
+
request_id: positiveID.optional(),
|
|
244
|
+
project_ref: z.string().optional(),
|
|
245
|
+
action_name: z.string().optional(),
|
|
246
|
+
input: z.union([vaultGenerateInputSchema, vaultSessionInputSchema]).optional(),
|
|
247
|
+
reason: z.string().optional(),
|
|
248
|
+
created_at: z.string().optional(),
|
|
249
|
+
expires_at: z.string().optional(),
|
|
250
|
+
secret_values_returned: z.literal(false).optional(),
|
|
251
|
+
output: z.union([generatedVaultOutputSchema, vaultSessionOutputSchema]).optional(),
|
|
252
|
+
output_withheld: z.boolean().optional(),
|
|
253
|
+
retry_after_seconds: nonNegativeInteger.optional(),
|
|
254
|
+
assistant_hint: z.string().optional(),
|
|
255
|
+
error: z.string().optional(),
|
|
256
|
+
})
|
|
257
|
+
.strict();
|
|
258
|
+
|
|
259
|
+
export const responseContracts = Object.freeze({
|
|
260
|
+
connectorTargets: z.array(connectorTargetSchema),
|
|
261
|
+
connectorHelp: connectorHelpSchema,
|
|
262
|
+
connectorActions: connectorActionsSchema,
|
|
263
|
+
connectorActionCall: connectorActionCallSchema,
|
|
264
|
+
connectorActionRequest: connectorActionRequestSchema,
|
|
265
|
+
vaultItems: vaultItemsSchema,
|
|
266
|
+
vaultAction: vaultActionResponseSchema,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
export function projectGatewaySuccess(schema, value) {
|
|
270
|
+
const result = schema.safeParse(value);
|
|
271
|
+
if (!result.success) {
|
|
272
|
+
const error = new Error("Gateway success response failed MCP contract validation.");
|
|
273
|
+
error.code = "gateway_response_contract_invalid";
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
return result.data;
|
|
277
|
+
}
|
package/dist/results.js
CHANGED
|
@@ -45,10 +45,45 @@ 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, isFailure = () => false) {
|
|
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
|
+
const projected = project(value);
|
|
60
|
+
const result = textResult(projected);
|
|
61
|
+
return isFailure(projected) ? { ...result, isError: true } : result;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return errorResult(mutationContext ? projectionOutcomeUnknown(error, mutationContext) : error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
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
|
+
|
|
76
|
+
function projectionOutcomeUnknown(cause, context) {
|
|
77
|
+
const error = new Error(
|
|
78
|
+
"The gateway accepted the mutation, but its response failed MCP contract validation. The operation may have completed.",
|
|
79
|
+
{ cause },
|
|
80
|
+
);
|
|
81
|
+
error.resultStatus = "outcome_unknown";
|
|
82
|
+
error.code = "gateway_response_contract_outcome_unknown";
|
|
83
|
+
if (typeof context.idempotencyKey === "string") error.idempotencyKey = context.idempotencyKey;
|
|
84
|
+
if (Number.isSafeInteger(context.requestID) && context.requestID > 0) error.requestID = context.requestID;
|
|
85
|
+
error.assistantHint = context.idempotencyKey
|
|
86
|
+
? "Reconcile the original request using the same idempotency key and unchanged input. Never retry with a new key blindly."
|
|
87
|
+
: "Inspect the original request status before repeating the operation.";
|
|
88
|
+
return error;
|
|
54
89
|
}
|
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 {
|
|
19
|
+
import { projectGatewaySuccess, responseContracts } from "./response-contracts.js";
|
|
20
|
+
import { jsonActionToolResult, 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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
101
|
+
externalActionAnnotations,
|
|
102
|
+
async ({ target_ref, action_name, input, reason, idempotency_key }, { signal }) => {
|
|
103
|
+
return jsonActionToolResult(
|
|
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.connectorActionCall, 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
|
-
|
|
107
|
-
|
|
128
|
+
localReadAnnotations,
|
|
129
|
+
async ({ request_id }, { signal }) => {
|
|
130
|
+
return jsonToolResult(
|
|
131
|
+
() => apiGet(`/api/mcp/connector-action-requests/${request_id}`, { signal }),
|
|
132
|
+
(value) => projectGatewaySuccess(responseContracts.connectorActionRequest, 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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
159
|
+
externalActionAnnotations,
|
|
160
|
+
async ({ project_ref, action_name, input, reason, idempotency_key }, { signal }) => {
|
|
161
|
+
return jsonActionToolResult(
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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(
|
|
165
|
-
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
3
|
+
"version": "0.2.55",
|
|
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",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
53
53
|
"smol-toml": "1.8.0",
|
|
54
|
-
"yaml": "2.9.
|
|
54
|
+
"yaml": "2.9.1",
|
|
55
55
|
"zod": "3.25.76"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
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.55",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"identifier": "@aipermission/mcp",
|
|
11
|
-
"version": "0.2.
|
|
11
|
+
"version": "0.2.55",
|
|
12
12
|
"transport": {
|
|
13
13
|
"type": "stdio"
|
|
14
14
|
}
|