@aipermission/mcp 0.2.38 → 0.2.40
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 +58 -16
- package/dist/cli-flags.js +81 -0
- package/dist/cli.js +37 -6
- package/dist/client-registry.js +281 -0
- package/dist/doctor.js +134 -0
- package/dist/init.js +235 -161
- package/dist/install-skill.js +66 -151
- package/dist/instructions.js +1 -0
- package/dist/private-file.js +36 -0
- package/dist/server.js +8 -4
- package/package.json +4 -2
- package/server.json +2 -2
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { parse as parseTOML } from "smol-toml";
|
|
3
|
+
import { parseCommandFlags } from "./cli-flags.js";
|
|
4
|
+
import { adaptMCPServerConfig, resolveMCPConfigTarget, resolveSkillTarget } from "./client-registry.js";
|
|
5
|
+
import { buildMCPServerConfig, inspectProjectConfigProtection, sanitizeName } from "./init.js";
|
|
6
|
+
import { validateSkill } from "./install-skill.js";
|
|
7
|
+
import { normalizeLocalAPIURL } from "./local-url.js";
|
|
8
|
+
import { assertPrivateFilePermissions, assertTrustedFilePath } from "./private-file.js";
|
|
9
|
+
|
|
10
|
+
export async function runDoctor(argv = []) {
|
|
11
|
+
const flags = parseCommandFlags("doctor", argv);
|
|
12
|
+
const client = flags.client || flags.provider;
|
|
13
|
+
if (!client) throw new Error("doctor requires --client.");
|
|
14
|
+
const result = await inspectClientSetup({
|
|
15
|
+
client,
|
|
16
|
+
scope: flags.scope,
|
|
17
|
+
mcpScope: flags.mcpScope,
|
|
18
|
+
skillScope: flags.skillScope,
|
|
19
|
+
name: flags.name || "aipermission",
|
|
20
|
+
homeDir: flags.home,
|
|
21
|
+
projectDir: flags.projectDir,
|
|
22
|
+
});
|
|
23
|
+
console.log(`AIPermission MCP doctor: ${result.client} (${result.scope})`);
|
|
24
|
+
for (const entry of result.checks) console.log(`${entry.ok ? "PASS" : "FAIL"} ${entry.label}: ${entry.message}`);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function inspectClientSetup({
|
|
29
|
+
client,
|
|
30
|
+
scope,
|
|
31
|
+
mcpScope,
|
|
32
|
+
skillScope,
|
|
33
|
+
name = "aipermission",
|
|
34
|
+
homeDir,
|
|
35
|
+
projectDir,
|
|
36
|
+
env,
|
|
37
|
+
platform,
|
|
38
|
+
execFile,
|
|
39
|
+
}) {
|
|
40
|
+
const serverName = sanitizeName(name);
|
|
41
|
+
const roots = { homeDir, projectDir, env };
|
|
42
|
+
const configTarget = resolveMCPConfigTarget(client, mcpScope || scope, roots);
|
|
43
|
+
const skillTarget = resolveSkillTarget(client, skillScope || scope, roots);
|
|
44
|
+
const checks = [await inspectConfig(configTarget, serverName, { projectDir, platform, execFile }), await inspectSkill(skillTarget)];
|
|
45
|
+
return {
|
|
46
|
+
ok: checks.every((entry) => entry.ok),
|
|
47
|
+
client: configTarget.label,
|
|
48
|
+
scope: configTarget.scope === skillTarget.scope ? configTarget.scope : `MCP ${configTarget.scope}, skill ${skillTarget.scope}`,
|
|
49
|
+
checks,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function inspectConfig(target, name, options) {
|
|
54
|
+
try {
|
|
55
|
+
await assertTrustedFilePath(target.path, { trustedRoot: target.trustedRoot });
|
|
56
|
+
await assertPrivateFilePermissions(target.path, options);
|
|
57
|
+
if (target.projectConfig) {
|
|
58
|
+
await inspectProjectConfigProtection(target.path, options.projectDir || target.trustedRoot);
|
|
59
|
+
}
|
|
60
|
+
const contents = await fs.readFile(target.path, "utf8");
|
|
61
|
+
const server = target.format === "json" ? readJSONServer(contents, target.rootKey, name) : readTOMLServer(contents, name);
|
|
62
|
+
if (!server) return check(false, "MCP config", `server ${name} is missing from ${target.path}`);
|
|
63
|
+
validateServer(target.client, server);
|
|
64
|
+
return check(true, "MCP config", `valid at ${target.path}`);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (error.code === "ENOENT") return check(false, "MCP config", `not found at ${target.path}`);
|
|
67
|
+
return check(false, "MCP config", `invalid at ${target.path}: ${safeErrorMessage(error)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function inspectSkill(target) {
|
|
72
|
+
try {
|
|
73
|
+
await assertTrustedFilePath(target.path, { trustedRoot: target.trustedRoot });
|
|
74
|
+
validateSkill(await fs.readFile(target.path, "utf8"));
|
|
75
|
+
return check(true, "Operator skill", `valid at ${target.path}`);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error.code === "ENOENT") return check(false, "Operator skill", `not found at ${target.path}`);
|
|
78
|
+
return check(false, "Operator skill", `invalid at ${target.path}: ${safeErrorMessage(error)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readJSONServer(contents, rootKey, name) {
|
|
83
|
+
let root;
|
|
84
|
+
try {
|
|
85
|
+
root = JSON.parse(contents);
|
|
86
|
+
} catch {
|
|
87
|
+
throw new Error("JSON parsing failed; no file contents were included in this diagnostic");
|
|
88
|
+
}
|
|
89
|
+
const server = root?.[rootKey]?.[name];
|
|
90
|
+
return server && typeof server === "object" && !Array.isArray(server) ? server : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readTOMLServer(contents, name) {
|
|
94
|
+
let root;
|
|
95
|
+
try {
|
|
96
|
+
root = parseTOML(contents);
|
|
97
|
+
} catch {
|
|
98
|
+
throw new Error("TOML parsing failed; no file contents were included in this diagnostic");
|
|
99
|
+
}
|
|
100
|
+
const server = root?.mcp_servers?.[name];
|
|
101
|
+
return server && typeof server === "object" && !Array.isArray(server) ? server : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function validateServer(client, server) {
|
|
105
|
+
const expected = adaptMCPServerConfig(client, buildMCPServerConfig({ apiUrl: "http://localhost:3210", token: "TOKEN" }));
|
|
106
|
+
if (server.command !== expected.command) throw new Error("server does not use npx");
|
|
107
|
+
if (!sameArray(server.args, expected.args)) throw new Error(`server does not use the exact ${expected.args[1]} command arguments`);
|
|
108
|
+
if (server.env?.NODE_ENV !== "production") throw new Error("server NODE_ENV is not production");
|
|
109
|
+
if (typeof server.env?.AIPERMISSION_API_TOKEN !== "string" || !server.env.AIPERMISSION_API_TOKEN) {
|
|
110
|
+
throw new Error("server has no API token");
|
|
111
|
+
}
|
|
112
|
+
normalizeLocalAPIURL(server.env?.AIPERMISSION_API_URL);
|
|
113
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
114
|
+
if (["command", "args", "env"].includes(key)) continue;
|
|
115
|
+
if (!sameValue(server[key], value)) throw new Error(`server has an invalid ${key} field for this client`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function sameArray(left, right) {
|
|
120
|
+
return Array.isArray(left) && left.length === right.length && left.every((value, index) => value === right[index]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sameValue(left, right) {
|
|
124
|
+
return Array.isArray(right) ? sameArray(left, right) : left === right;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function safeErrorMessage(error) {
|
|
128
|
+
if (/parsing failed/i.test(error.message || "")) return error.message;
|
|
129
|
+
return String(error.message || error).replace(/[\r\n]+/g, " ");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function check(ok, label, message) {
|
|
133
|
+
return { ok, label, message };
|
|
134
|
+
}
|