phistruct-mcp 0.1.0 → 0.1.2
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/cli.mjs +137 -23
- package/package.json +2 -2
package/cli.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
2
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { createConnection } from "node:net";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
|
-
import { join, resolve } from "node:path";
|
|
7
|
-
import {
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
9
9
|
export const BUNDLE_ID = "com.phistruct.app";
|
|
10
10
|
export const BINARY_NAME = "PhiStruct";
|
|
@@ -71,8 +71,24 @@ function runMdfind() {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/** A visible PhiStruct window, including a different copy than `bin`. Ignores the hidden --mcp helper. */
|
|
75
|
+
export function isGuiCommand(command, bin) {
|
|
76
|
+
if (!command || isStdioMcpCommand(command)) return false;
|
|
77
|
+
if (bin && command.includes(bin)) return true;
|
|
78
|
+
return /\/MacOS\/PhiStruct(?:\s|$)/.test(command);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function runningGuiBinary(listCommands = psCommands) {
|
|
82
|
+
for (const command of listCommands()) {
|
|
83
|
+
if (!isGuiCommand(command)) continue;
|
|
84
|
+
const match = command.match(/(\/\S+\/MacOS\/PhiStruct)(?:\s|$)/);
|
|
85
|
+
if (match) return match[1];
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
74
90
|
export function guiIsRunning(bin, listCommands = psCommands) {
|
|
75
|
-
return listCommands().some((command) => command
|
|
91
|
+
return listCommands().some((command) => isGuiCommand(command, bin));
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
function psCommands() {
|
|
@@ -116,34 +132,132 @@ export async function waitForLiveMcp({
|
|
|
116
132
|
return null;
|
|
117
133
|
}
|
|
118
134
|
|
|
135
|
+
export const SERVER_NAME = "phistruct";
|
|
136
|
+
export const SERVER_ENTRY = {
|
|
137
|
+
command: "npx",
|
|
138
|
+
args: ["-y", "phistruct-mcp"],
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export function mergeMcpDocument(raw, name = SERVER_NAME, entry = SERVER_ENTRY) {
|
|
142
|
+
const text = (raw ?? "").replace(/^\uFEFF/, "").trim();
|
|
143
|
+
let data = {};
|
|
144
|
+
if (text) {
|
|
145
|
+
data = JSON.parse(text);
|
|
146
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
147
|
+
throw new Error("MCP config is not a JSON object");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (
|
|
151
|
+
!data.mcpServers ||
|
|
152
|
+
typeof data.mcpServers !== "object" ||
|
|
153
|
+
Array.isArray(data.mcpServers)
|
|
154
|
+
) {
|
|
155
|
+
data.mcpServers = {};
|
|
156
|
+
}
|
|
157
|
+
data.mcpServers[name] = { ...entry };
|
|
158
|
+
return data;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function defaultConfigPaths(home = homedir(), platform = process.platform) {
|
|
162
|
+
const paths = [{ path: join(home, ".cursor", "mcp.json"), create: "always" }];
|
|
163
|
+
if (platform === "darwin") {
|
|
164
|
+
paths.push({
|
|
165
|
+
path: join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
|
|
166
|
+
create: "if-parent",
|
|
167
|
+
});
|
|
168
|
+
} else if (platform === "win32") {
|
|
169
|
+
const appdata = process.env.APPDATA;
|
|
170
|
+
if (appdata) {
|
|
171
|
+
paths.push({ path: join(appdata, "Claude", "claude_desktop_config.json"), create: "if-parent" });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
paths.push({ path: join(home, ".claude.json"), create: "if-file" });
|
|
175
|
+
return paths;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function shouldWriteConfig({ path, create }, exists = existsSync) {
|
|
179
|
+
if (create === "always") return true;
|
|
180
|
+
if (create === "if-file") return exists(path);
|
|
181
|
+
return exists(path) || exists(dirname(path));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function writeMergedConfig(path, raw) {
|
|
185
|
+
const data = mergeMcpDocument(raw);
|
|
186
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
187
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
188
|
+
writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`);
|
|
189
|
+
renameSync(tmp, path);
|
|
190
|
+
return path;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function setupConfigs({
|
|
194
|
+
home = homedir(),
|
|
195
|
+
platform = process.platform,
|
|
196
|
+
exists = existsSync,
|
|
197
|
+
read = (p) => readFileSync(p, "utf8"),
|
|
198
|
+
} = {}) {
|
|
199
|
+
const written = [];
|
|
200
|
+
for (const spec of defaultConfigPaths(home, platform)) {
|
|
201
|
+
if (!shouldWriteConfig(spec, exists)) continue;
|
|
202
|
+
const raw = exists(spec.path) ? read(spec.path) : "";
|
|
203
|
+
writeMergedConfig(spec.path, raw);
|
|
204
|
+
written.push(spec.path);
|
|
205
|
+
}
|
|
206
|
+
if (written.length === 0) {
|
|
207
|
+
throw new Error("No MCP config files to write");
|
|
208
|
+
}
|
|
209
|
+
return written;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export const WINDOW_CLOSED_MESSAGE = "PhiStruct 窗口没开。请先自己打开 PhiStruct。";
|
|
213
|
+
|
|
119
214
|
function fail(message) {
|
|
120
215
|
process.stderr.write(`${message}\n`);
|
|
121
216
|
process.exit(1);
|
|
122
217
|
}
|
|
123
218
|
|
|
219
|
+
/** Use the app's own helper so sandboxed UserDefaults stay inside the app container. */
|
|
220
|
+
export function runStdioBridge({
|
|
221
|
+
bin = runningGuiBinary(),
|
|
222
|
+
spawnProcess = spawn,
|
|
223
|
+
onExit = (code) => process.exit(code ?? 1),
|
|
224
|
+
} = {}) {
|
|
225
|
+
if (!bin) throw new Error(WINDOW_CLOSED_MESSAGE);
|
|
226
|
+
const child = spawnProcess(bin, ["--mcp"], { stdio: "inherit" });
|
|
227
|
+
child.once("error", (err) => {
|
|
228
|
+
process.stderr.write(`Could not connect to PhiStruct MCP: ${err.message}\n`);
|
|
229
|
+
onExit(1);
|
|
230
|
+
});
|
|
231
|
+
child.once("exit", (code, signal) => {
|
|
232
|
+
if (signal) process.kill(process.pid, signal);
|
|
233
|
+
else onExit(code);
|
|
234
|
+
});
|
|
235
|
+
return child;
|
|
236
|
+
}
|
|
237
|
+
|
|
124
238
|
async function main() {
|
|
125
|
-
|
|
126
|
-
|
|
239
|
+
const argv = process.argv.slice(2).filter((a) => a !== "--");
|
|
240
|
+
if (argv[0] === "setup" || argv[0] === "install") {
|
|
241
|
+
const written = setupConfigs();
|
|
242
|
+
for (const path of written) process.stderr.write(`Wrote ${path}\n`);
|
|
243
|
+
return;
|
|
127
244
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
fail("PhiStruct.app not found. Install PhiStruct, then try again.");
|
|
131
|
-
}
|
|
132
|
-
if (!guiIsRunning(bin)) {
|
|
133
|
-
spawn(bin, [], { detached: true, stdio: "ignore" }).unref();
|
|
245
|
+
if (process.platform !== "darwin") {
|
|
246
|
+
fail("PhiStruct MCP only runs on macOS.");
|
|
134
247
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
248
|
+
runStdioBridge();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function isDirectRun() {
|
|
252
|
+
const argv1 = process.argv[1];
|
|
253
|
+
if (!argv1) return false;
|
|
254
|
+
try {
|
|
255
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(resolve(argv1));
|
|
256
|
+
} catch {
|
|
257
|
+
return false;
|
|
138
258
|
}
|
|
139
|
-
const child = spawn(bin, ["--mcp"], { stdio: "inherit" });
|
|
140
|
-
child.on("exit", (code, signal) => {
|
|
141
|
-
if (signal) process.kill(process.pid, signal);
|
|
142
|
-
process.exit(code ?? 1);
|
|
143
|
-
});
|
|
144
259
|
}
|
|
145
260
|
|
|
146
|
-
|
|
147
|
-
if (import.meta.url === entry) {
|
|
261
|
+
if (isDirectRun()) {
|
|
148
262
|
main().catch((err) => fail(err instanceof Error ? err.message : String(err)));
|
|
149
263
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phistruct-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "MCP stdio bridge
|
|
5
|
+
"description": "MCP stdio bridge for an already-open PhiStruct window. Does not launch the app.",
|
|
6
6
|
"bin": {
|
|
7
7
|
"phistruct-mcp": "./cli.mjs"
|
|
8
8
|
},
|