@aipermission/mcp 0.2.56 → 0.2.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,6 +41,11 @@ configuration, and installs the native operator skill for the selected client.
41
41
  Generated runtime configs pin the exact package version that wrote them; re-run
42
42
  setup when you intentionally upgrade a client. Use `init` when you only want the
43
43
  MCP config, or `install-skill` when you only want the skill.
44
+ VS Code `.vscode/mcp.json` may contain JSONC comments and trailing commas;
45
+ setup updates only the selected server entry while keeping unrelated entries
46
+ and comments. Setup validates the skill before writing the config, then installs
47
+ it. If skill installation fails after the config is written, the error names
48
+ that partial state so you can run `install-skill` separately.
44
49
 
45
50
  Check both paths without printing the bearer token:
46
51
 
@@ -91,6 +96,8 @@ integer; invalid configuration stops the bridge instead of silently changing
91
96
  the deadline. It covers both response headers and the complete response body,
92
97
  including streamed bodies. A timeout does not prove that a submitted
93
98
  operation failed; do not retry mutations with a new idempotency key blindly.
99
+ Gateway response bodies are capped at 8 MiB. An oversized POST response is
100
+ also treated as an unknown outcome, not proof that the operation failed.
94
101
  If a POST response is lost or incomplete, the bridge returns
95
102
  `status: outcome_unknown` and `code: gateway_transport_outcome_unknown`.
96
103
  Action calls also return the original `idempotency_key`; no request ID is
package/dist/init.js CHANGED
@@ -11,6 +11,7 @@ import { parseCommandFlags } from "./cli-flags.js";
11
11
  import { DEFAULT_API_URL, normalizeLocalAPIURL } from "./local-url.js";
12
12
  import { adaptMCPServerConfig, getClient, MCP_PROVIDERS, resolveMCPConfigTarget, resolveMCPPrintTarget } from "./client-registry.js";
13
13
  import { commitSkillInstallation, prepareSkillInstallation } from "./install-skill.js";
14
+ import { updateJSONCServer } from "./jsonc-config.js";
14
15
  import {
15
16
  atomicWritePrivateFile,
16
17
  privateLockPath,
@@ -50,12 +51,18 @@ async function runConfiguration(command, argv) {
50
51
  const flags = parseCommandFlags(command, argv);
51
52
  const interactive = Boolean(input.isTTY && output.isTTY);
52
53
  assertProviderSelectionAvailable(flags.provider, interactive);
53
- const rl = readline.createInterface({ input, output });
54
+ let rl;
55
+ const getReadline = () => {
56
+ rl ||= readline.createInterface({ input, output });
57
+ return rl;
58
+ };
54
59
  try {
55
60
  const provider = flags.provider
56
61
  ? findProvider(flags.provider)
57
62
  : await selectProvider("Which AI client should use this token?", MCP_PROVIDERS);
58
- const name = sanitizeName(flags.name || (interactive ? await ask(rl, "MCP server name", "aipermission") : "aipermission"));
63
+ const needsToken = provider.id !== "custom" && !flags.print;
64
+ const stdinToken = needsToken && flags.tokenStdin ? (await readStdin()).trim() : "";
65
+ const name = sanitizeName(flags.name || (interactive ? await ask(getReadline(), "MCP server name", "aipermission") : "aipermission"));
59
66
  const apiUrl = normalizeURL(flags.apiUrl || DEFAULT_API_URL);
60
67
  const outputTarget =
61
68
  provider.id === "custom"
@@ -75,19 +82,30 @@ async function runConfiguration(command, argv) {
75
82
  return { provider: provider.id, name, printed: true, skill: skillResult };
76
83
  }
77
84
 
78
- const stdinToken = flags.tokenStdin ? (await readStdin()).trim() : "";
79
- const token = await resolveToken({ ...flags, stdinToken }, rl);
85
+ const token = flags.tokenStdin ? stdinToken : await resolveToken(flags, getReadline());
80
86
  if (!token) {
81
87
  throw new Error("API token is required.");
82
88
  }
83
89
  const config = adaptMCPServerConfig(provider.id, buildMCPServerConfig({ apiUrl, token }));
84
- const skillResult = preparedSkill ? await reportInstalledSkill(preparedSkill) : undefined;
85
90
  const result = await writeProviderConfig(provider.id, name, config, {
86
91
  force: Boolean(flags.force),
87
92
  scope: flags.scope,
88
93
  homeDir: flags.home,
89
94
  projectDir: flags.projectDir,
90
95
  });
96
+ let skillResult;
97
+ if (preparedSkill) {
98
+ try {
99
+ skillResult = await reportInstalledSkill(preparedSkill);
100
+ } catch (error) {
101
+ throw new Error(
102
+ `MCP config was written at ${result.path}, but operator skill installation failed. Complete the skill installation separately.`,
103
+ {
104
+ cause: error,
105
+ },
106
+ );
107
+ }
108
+ }
91
109
  console.log("");
92
110
  console.log(`${color.green}Configured ${provider.label}${color.reset}`);
93
111
  console.log(`${color.dim}Name:${color.reset} ${name}`);
@@ -103,7 +121,7 @@ async function runConfiguration(command, argv) {
103
121
  console.log(`${color.yellow}Restart the AI client so it reloads MCP servers.${color.reset}`);
104
122
  return { provider: provider.id, name, config: result, skill: skillResult };
105
123
  } finally {
106
- rl.close();
124
+ rl?.close();
107
125
  }
108
126
  }
109
127
 
@@ -311,6 +329,7 @@ export async function writeProviderConfig(providerID, name, config, options = {}
311
329
  const trustedRoot = target.trustedRoot;
312
330
  const writeOptions = {
313
331
  trustedRoot,
332
+ jsonc: providerID === "vscode",
314
333
  beforeWrite: target.projectConfig
315
334
  ? async () => {
316
335
  await assertProjectConfigWritable(target.path, options);
@@ -332,27 +351,39 @@ export async function writeJSONMCPConfig(filePath, name, config, rootKey, option
332
351
  filePath,
333
352
  async () => {
334
353
  await options.beforeWrite?.();
335
- let root = {};
354
+ let content = "";
336
355
  try {
337
- root = JSON.parse(await fs.readFile(filePath, "utf8"));
356
+ content = await fs.readFile(filePath, "utf8");
338
357
  } catch (error) {
339
358
  if (error.code !== "ENOENT") {
340
- redactParseError(error);
341
- throw new Error(`Could not parse JSON config at ${filePath}; the existing file was left unchanged`, {
359
+ throw new Error(`Could not read JSON config at ${filePath}; the existing file was left unchanged`, {
342
360
  cause: error,
343
361
  });
344
362
  }
345
363
  }
346
- if (!root || typeof root !== "object" || Array.isArray(root)) root = {};
347
- const currentServers = root[rootKey];
348
- const servers =
349
- currentServers && typeof currentServers === "object" && !Array.isArray(currentServers)
350
- ? { ...currentServers }
351
- : Object.create(null);
352
- Object.defineProperty(servers, name, { value: config, enumerable: true, configurable: true, writable: true });
353
- root[rootKey] = servers;
364
+ let outputContent;
365
+ if (options.jsonc) {
366
+ outputContent = updateJSONCServer(content, rootKey, name, config);
367
+ } else {
368
+ let root;
369
+ try {
370
+ root = content ? JSON.parse(content) : {};
371
+ } catch (error) {
372
+ redactParseError(error);
373
+ throw new Error(`Could not parse JSON config at ${filePath}; the existing file was left unchanged`, { cause: error });
374
+ }
375
+ if (!root || typeof root !== "object" || Array.isArray(root)) root = {};
376
+ const currentServers = root[rootKey];
377
+ const servers =
378
+ currentServers && typeof currentServers === "object" && !Array.isArray(currentServers)
379
+ ? { ...currentServers }
380
+ : Object.create(null);
381
+ Object.defineProperty(servers, name, { value: config, enumerable: true, configurable: true, writable: true });
382
+ root[rootKey] = servers;
383
+ outputContent = `${JSON.stringify(root, null, 2)}\n`;
384
+ }
354
385
  await options.beforeWrite?.();
355
- await writePrivateFile(filePath, `${JSON.stringify(root, null, 2)}\n`, options);
386
+ await writePrivateFile(filePath, outputContent, options);
356
387
  },
357
388
  options,
358
389
  );
@@ -0,0 +1,33 @@
1
+ import { applyEdits, modify, parse } from "jsonc-parser";
2
+
3
+ export function updateJSONCServer(content, rootKey, name, config) {
4
+ const source = content.trim() ? content : "{}\n";
5
+ const errors = [];
6
+ const root = parse(source, errors, { allowTrailingComma: true });
7
+ if (errors.length || !root || typeof root !== "object" || Array.isArray(root)) {
8
+ throw new Error("Could not parse VS Code JSONC config; the existing file was left unchanged.");
9
+ }
10
+ const servers = root[rootKey];
11
+ if (servers !== undefined && (!servers || typeof servers !== "object" || Array.isArray(servers))) {
12
+ throw new Error("VS Code MCP servers must be an object; the existing file was left unchanged.");
13
+ }
14
+ const eol = source.includes("\r\n") ? "\r\n" : "\n";
15
+ let next;
16
+ try {
17
+ next = applyEdits(
18
+ source,
19
+ modify(source, [rootKey, name], config, {
20
+ getInsertionIndex: () => 0,
21
+ formattingOptions: { insertSpaces: true, tabSize: 2, eol },
22
+ }),
23
+ );
24
+ } catch {
25
+ throw new Error("Could not safely update VS Code JSONC config; the existing file was left unchanged.");
26
+ }
27
+ const nextErrors = [];
28
+ const updated = parse(next, nextErrors, { allowTrailingComma: true });
29
+ if (nextErrors.length || JSON.stringify(updated?.[rootKey]?.[name]) !== JSON.stringify(config)) {
30
+ throw new Error("Could not safely update VS Code JSONC config; the existing file was left unchanged.");
31
+ }
32
+ return next;
33
+ }
@@ -0,0 +1,23 @@
1
+ export const MAX_GATEWAY_RESPONSE_BYTES = 8 * 1024 * 1024;
2
+
3
+ export async function readGatewayResponseText(response) {
4
+ if (!response.body) return "";
5
+ const reader = response.body.getReader();
6
+ const chunks = [];
7
+ let received = 0;
8
+ try {
9
+ for (;;) {
10
+ const { done, value } = await reader.read();
11
+ if (done) break;
12
+ received += value.byteLength;
13
+ if (received > MAX_GATEWAY_RESPONSE_BYTES) {
14
+ await reader.cancel();
15
+ throw new Error(`Gateway response exceeds ${MAX_GATEWAY_RESPONSE_BYTES} bytes.`);
16
+ }
17
+ chunks.push(value);
18
+ }
19
+ } finally {
20
+ reader.releaseLock();
21
+ }
22
+ return new TextDecoder().decode(Buffer.concat(chunks, received));
23
+ }
package/dist/server.js CHANGED
@@ -18,6 +18,7 @@ import { idempotencyKeySchema } from "./idempotency-key.js";
18
18
  import { normalizeLocalAPIURL } from "./local-url.js";
19
19
  import { projectGatewaySuccess, responseContracts } from "./response-contracts.js";
20
20
  import { jsonActionToolResult, jsonToolResult } from "./results.js";
21
+ import { readGatewayResponseText } from "./response-body.js";
21
22
  import { externalActionAnnotations, localMutationAnnotations, localReadAnnotations } from "./tool-annotations.js";
22
23
 
23
24
  const packageMetadata = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
@@ -266,7 +267,7 @@ async function apiRequest(path, options, idempotencyKey, context = {}) {
266
267
  }
267
268
  dispatchStarted = true;
268
269
  const response = await fetch(request, { signal: controller.signal });
269
- const text = await response.text();
270
+ const text = await readGatewayResponseText(response);
270
271
  const data = response.status === 204 ? null : parseResponseBody(text);
271
272
  bodyReceived = true;
272
273
  if (!response.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aipermission/mcp",
3
- "version": "0.2.56",
3
+ "version": "0.2.58",
4
4
  "mcpName": "io.github.aipermission/aipermission-mcp",
5
5
  "description": "Local-only MCP bridge for the aipermission gateway.",
6
6
  "license": "AGPL-3.0-only",
@@ -50,6 +50,7 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@modelcontextprotocol/sdk": "1.30.0",
53
+ "jsonc-parser": "3.3.1",
53
54
  "smol-toml": "1.8.0",
54
55
  "yaml": "2.9.1",
55
56
  "zod": "3.25.76"
package/server.json CHANGED
@@ -3,12 +3,12 @@
3
3
  "name": "io.github.aipermission/aipermission-mcp",
4
4
  "title": "AIPermission",
5
5
  "description": "Local-only MCP bridge for the AIPermission gateway.",
6
- "version": "0.2.56",
6
+ "version": "0.2.58",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "@aipermission/mcp",
11
- "version": "0.2.56",
11
+ "version": "0.2.58",
12
12
  "transport": {
13
13
  "type": "stdio"
14
14
  }