@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 +7 -5
- package/dist/response-contracts.js +264 -0
- package/dist/results.js +26 -2
- package/dist/server.js +116 -49
- package/dist/tool-annotations.js +20 -0
- package/package.json +2 -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
|
|
@@ -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
|
|
@@ -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
|
@@ -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
|
-
|
|
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 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
|
-
|
|
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.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
|
-
|
|
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 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
|
-
|
|
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.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",
|
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
|
}
|