@uzqw/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/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { writeFileSync } from "fs";
|
|
|
8
8
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
9
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
10
|
import open from "open";
|
|
11
|
-
import { RecolxClient, runOAuthCallback, LoginInput, LogoutInput } from "@uzqw/shared";
|
|
11
|
+
import { RecolxClient, runOAuthCallback, LoginInput, LogoutInput, SetEnvInput, GetEnvInput } from "@uzqw/shared";
|
|
12
12
|
|
|
13
13
|
// src/tools.ts
|
|
14
14
|
import {
|
|
@@ -237,11 +237,12 @@ var CALLBACK_PORT = 8199;
|
|
|
237
237
|
var LOGIN_TIMEOUT_MS = 12e4;
|
|
238
238
|
var sub = process.argv[2];
|
|
239
239
|
if (sub === "install" || sub === "uninstall") {
|
|
240
|
-
const { runInstall, runUninstall } = await import("./install-
|
|
240
|
+
const { runInstall, runUninstall, parseEnvFlag } = await import("./install-D7FNIM42.js");
|
|
241
241
|
const args = process.argv.slice(3);
|
|
242
242
|
const yes = args.includes("--yes") || args.includes("-y");
|
|
243
243
|
const noLogin = args.includes("--no-login");
|
|
244
|
-
|
|
244
|
+
const env = parseEnvFlag(args);
|
|
245
|
+
if (sub === "install") await runInstall({ yes, noLogin, env: env ?? void 0 });
|
|
245
246
|
else await runUninstall({ yes });
|
|
246
247
|
process.exit(0);
|
|
247
248
|
}
|
|
@@ -252,6 +253,7 @@ if (sub === "--help" || sub === "-h") {
|
|
|
252
253
|
recolx-mcp \u4EE5 stdio \u542F\u52A8 MCP server\uFF08Claude/Codex/Cursor/Zed \u7B49\u5BA2\u6237\u7AEF\u9ED8\u8BA4\u8DEF\u5F84\uFF09
|
|
253
254
|
recolx-mcp install \u68C0\u6D4B\u5E76\u5199\u5165\u672C\u5730\u5BA2\u6237\u7AEF\u914D\u7F6E\uFF08Claude Desktop / Codex / Claude Code / Cursor / Windsurf / VS Code / Zed\uFF09
|
|
254
255
|
recolx-mcp install --yes \u5168\u81EA\u52A8\uFF1A\u4E0D\u8BE2\u95EE\uFF0C\u76F4\u63A5\u914D\u7F6E\u6240\u6709\u68C0\u6D4B\u5230\u7684\u5BA2\u6237\u7AEF
|
|
256
|
+
recolx-mcp install --env local|staging|prod \u5207\u6362\u73AF\u5883\uFF08\u5199 ~/.recolx/config.json\uFF0C\u9ED8\u8BA4 prod\uFF09
|
|
255
257
|
recolx-mcp install --no-login \u8DF3\u8FC7 OAuth \u767B\u5F55\uFF08CI / \u8DF3\u677F\u673A\uFF09
|
|
256
258
|
recolx-mcp uninstall \u79FB\u9664\u5404\u5BA2\u6237\u7AEF\u91CC\u7684 recolx MCP \u914D\u7F6E
|
|
257
259
|
`);
|
|
@@ -263,7 +265,7 @@ var READ_ONLY2 = { readOnlyHint: true, destructiveHint: false, openWorldHint: tr
|
|
|
263
265
|
server.registerTool(
|
|
264
266
|
"login",
|
|
265
267
|
{
|
|
266
|
-
description: "\u767B\u5F55 Recolx\uFF08OAuth PKCE\uFF09\uFF1A\u6253\u5F00\u6D4F\u89C8\u5668\u8DF3\u8F6C\u6388\u6743\u9875\uFF0C\u540C\u610F\u540E\u5728\u672C\u5730 8199 \u56DE\u8C03\u6536\u53D6 token \u5E76\u5199\u5165 ~/.recolx/tokens-mcp.json\uFF1B\u5DF2\u767B\u5F55\u65F6\u76F4\u63A5\u8FD4\u56DE",
|
|
268
|
+
description: "\u767B\u5F55 Recolx\uFF08OAuth PKCE\uFF09\uFF1A\u6253\u5F00\u6D4F\u89C8\u5668\u8DF3\u8F6C\u6388\u6743\u9875\uFF0C\u540C\u610F\u540E\u5728\u672C\u5730 8199 \u56DE\u8C03\u6536\u53D6 token \u5E76\u5199\u5165 ~/.recolx/tokens-mcp-{env}.json\uFF08\u6BCF\u73AF\u5883\u72EC\u7ACB\uFF09\uFF1B\u5DF2\u767B\u5F55\u65F6\u76F4\u63A5\u8FD4\u56DE",
|
|
267
269
|
annotations: { title: "\u767B\u5F55 Recolx", ...READ_ONLY2 },
|
|
268
270
|
inputSchema: LoginInput
|
|
269
271
|
},
|
|
@@ -362,6 +364,42 @@ server.registerTool(
|
|
|
362
364
|
}
|
|
363
365
|
);
|
|
364
366
|
registerTools(server, client);
|
|
367
|
+
var ENV_TOOL = { destructiveHint: false, openWorldHint: true };
|
|
368
|
+
server.registerTool(
|
|
369
|
+
"set_env",
|
|
370
|
+
{
|
|
371
|
+
description: "\u5207\u6362 Recolx \u73AF\u5883\uFF08local \u672C\u5730\u6D4B\u8BD5 / staging \u7EBF\u4E0A\u6D4B\u8BD5 / prod \u7EBF\u4E0A\u6B63\u5F0F\uFF09\uFF1A\u5199 ~/.recolx/config.json \u5E76\u5373\u65F6\u751F\u6548\uFF1B\u8FD4\u56DE\u65B0\u73AF\u5883\u3001\u540E\u7AEF\u5730\u5740\u4E0E\u767B\u5F55\u6001",
|
|
372
|
+
annotations: { title: "\u5207\u6362\u73AF\u5883", ...ENV_TOOL },
|
|
373
|
+
inputSchema: SetEnvInput
|
|
374
|
+
},
|
|
375
|
+
async ({ env }) => {
|
|
376
|
+
const state = await client.setEnv(env);
|
|
377
|
+
const login = state.logged_in ? "\u5DF2\u767B\u5F55" : "\u672A\u767B\u5F55\uFF08\u8BF7\u8FD0\u884C login\uFF09";
|
|
378
|
+
return { content: [{ type: "text", text: `\u5DF2\u5207\u6362\u5230 ${state.env}\uFF08${state.api_base}\uFF09\u3002${login}` }] };
|
|
379
|
+
}
|
|
380
|
+
);
|
|
381
|
+
server.registerTool(
|
|
382
|
+
"get_env",
|
|
383
|
+
{
|
|
384
|
+
description: "\u67E5\u770B\u5F53\u524D Recolx \u73AF\u5883\uFF08local/staging/prod\uFF09\u3001\u540E\u7AEF\u5730\u5740\u3001\u9274\u6743\u6A21\u5F0F\u4E0E\u767B\u5F55\u6001",
|
|
385
|
+
annotations: { title: "\u67E5\u770B\u73AF\u5883", ...READ_ONLY2 },
|
|
386
|
+
inputSchema: GetEnvInput
|
|
387
|
+
},
|
|
388
|
+
async () => {
|
|
389
|
+
const state = await client.getEnvState();
|
|
390
|
+
return {
|
|
391
|
+
content: [
|
|
392
|
+
{
|
|
393
|
+
type: "text",
|
|
394
|
+
text: `\u73AF\u5883\uFF1A${state.env}
|
|
395
|
+
\u540E\u7AEF\uFF1A${state.api_base}
|
|
396
|
+
\u9274\u6743\uFF1A${state.auth_mode}
|
|
397
|
+
\u767B\u5F55\uFF1A${state.logged_in ? "\u5DF2\u767B\u5F55" : "\u672A\u767B\u5F55"}`
|
|
398
|
+
}
|
|
399
|
+
]
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
);
|
|
365
403
|
try {
|
|
366
404
|
const skills = await loadSkills();
|
|
367
405
|
for (const skill of skills) {
|
|
@@ -12,10 +12,16 @@ import { dirname, join } from "path";
|
|
|
12
12
|
import { spawnSync } from "child_process";
|
|
13
13
|
import { createInterface } from "readline/promises";
|
|
14
14
|
import open from "open";
|
|
15
|
-
import { RecolxClient, runOAuthCallback } from "@uzqw/shared";
|
|
15
|
+
import { RecolxClient, writeEnvConfig, runOAuthCallback } from "@uzqw/shared";
|
|
16
16
|
var SERVER_NAME = "recolx";
|
|
17
17
|
var CALLBACK_PORT = 8199;
|
|
18
18
|
var LOGIN_TIMEOUT_MS = 12e4;
|
|
19
|
+
function parseEnvFlag(args) {
|
|
20
|
+
const i = args.indexOf("--env");
|
|
21
|
+
if (i === -1) return null;
|
|
22
|
+
const v = args[i + 1];
|
|
23
|
+
return v === "local" || v === "staging" || v === "prod" ? v : null;
|
|
24
|
+
}
|
|
19
25
|
function findNodeBinCommand(command) {
|
|
20
26
|
const commandName = platform() === "win32" ? `${command}.cmd` : command;
|
|
21
27
|
const sibling = join(dirname(process.execPath), commandName);
|
|
@@ -30,10 +36,9 @@ function isLocalPackageSpec(spec) {
|
|
|
30
36
|
}
|
|
31
37
|
function getMcpEntry() {
|
|
32
38
|
const packageSpec = process.env.RECOLX_MCP_PACKAGE_SPEC?.trim() || "@uzqw/mcp@latest";
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
return { command: findNodeBinCommand("npx"), args: ["-y", packageSpec] };
|
|
39
|
+
const base = isLocalPackageSpec(packageSpec) ? { command: findNodeBinCommand("npm"), args: ["exec", "--yes", "--package", packageSpec, "--", "recolx-mcp"] } : { command: findNodeBinCommand("npx"), args: ["-y", packageSpec] };
|
|
40
|
+
const env = process.env.RECOLX_API_BASE?.trim() ? { RECOLX_API_BASE: process.env.RECOLX_API_BASE.trim().replace(/\/+$/, "") } : {};
|
|
41
|
+
return { ...base, env };
|
|
37
42
|
}
|
|
38
43
|
function commandPathIsStale(command) {
|
|
39
44
|
if (!command) return false;
|
|
@@ -64,6 +69,9 @@ function configPaths() {
|
|
|
64
69
|
claudeCodeDir: join(h, ".claude")
|
|
65
70
|
};
|
|
66
71
|
}
|
|
72
|
+
function entryEnv(e) {
|
|
73
|
+
return e.env && Object.keys(e.env).length > 0 ? e.env : void 0;
|
|
74
|
+
}
|
|
67
75
|
function isJsonObject(v) {
|
|
68
76
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
69
77
|
}
|
|
@@ -75,9 +83,18 @@ function entryAutoUpdates(entry) {
|
|
|
75
83
|
const args = Array.isArray(entry.args) ? entry.args : [];
|
|
76
84
|
return args.some((a) => typeof a === "string" && a.includes("@uzqw/mcp@latest"));
|
|
77
85
|
}
|
|
86
|
+
function entryEnvMatches(entry, expected) {
|
|
87
|
+
if (!isJsonObject(entry)) return false;
|
|
88
|
+
const env = isJsonObject(entry.env) ? entry.env : {};
|
|
89
|
+
if (!expected) return true;
|
|
90
|
+
return Object.entries(expected).every(([k, v]) => env[k] === v);
|
|
91
|
+
}
|
|
78
92
|
function jsonAdapter(spec) {
|
|
79
93
|
const rootKey = spec.rootKey ?? "mcpServers";
|
|
80
|
-
const toEntry = spec.toEntry ?? ((e) =>
|
|
94
|
+
const toEntry = spec.toEntry ?? ((e) => {
|
|
95
|
+
const env = entryEnv(e);
|
|
96
|
+
return env ? { command: e.command, args: e.args, env } : { command: e.command, args: e.args };
|
|
97
|
+
});
|
|
81
98
|
async function load() {
|
|
82
99
|
try {
|
|
83
100
|
const parsed = JSON.parse(await readFile(spec.configPath, "utf8"));
|
|
@@ -106,7 +123,7 @@ function jsonAdapter(spec) {
|
|
|
106
123
|
const existing = servers[SERVER_NAME];
|
|
107
124
|
if (existing && entryAutoUpdates(existing)) {
|
|
108
125
|
const cmd = isJsonObject(existing) && typeof existing.command === "string" ? existing.command : void 0;
|
|
109
|
-
if (!commandPathIsStale(cmd)) {
|
|
126
|
+
if (!commandPathIsStale(cmd) && entryEnvMatches(existing, entryEnv(ctx.entry))) {
|
|
110
127
|
return { status: "already-configured", message: "\u5DF2\u914D\u7F6E\uFF08npx @latest \u81EA\u52A8\u66F4\u65B0\uFF09", restartHint: spec.restartHint };
|
|
111
128
|
}
|
|
112
129
|
}
|
|
@@ -136,9 +153,25 @@ function codexBlockText(entry) {
|
|
|
136
153
|
const win = platform() === "win32";
|
|
137
154
|
const command = win ? normalizeWindowsPath(entry.command, "win32") : entry.command;
|
|
138
155
|
const args = (win ? entry.args.map((a) => normalizeWindowsPath(a, "win32")) : entry.args).map((a) => JSON.stringify(a)).join(", ");
|
|
156
|
+
const env = entryEnv(entry);
|
|
157
|
+
const envLine = env ? `
|
|
158
|
+
env = { ${Object.entries(env).map(([k, v]) => `${JSON.stringify(k)} = ${JSON.stringify(v)}`).join(", ")} }` : "";
|
|
139
159
|
return `${CODEX_HEADER}
|
|
140
160
|
command = ${JSON.stringify(command)}
|
|
141
|
-
args = [${args}]`;
|
|
161
|
+
args = [${args}]${envLine}`;
|
|
162
|
+
}
|
|
163
|
+
function extractTomlEnv(lines) {
|
|
164
|
+
for (const line of lines) {
|
|
165
|
+
const m = line.match(/^\s*env\s*=\s*\{(.*)\}\s*$/);
|
|
166
|
+
if (!m) continue;
|
|
167
|
+
const env = {};
|
|
168
|
+
for (const pair of m[1].split(",")) {
|
|
169
|
+
const kv = pair.match(/"([^"]+)"\s*=\s*"([^"]*)"/);
|
|
170
|
+
if (kv) env[kv[1]] = kv[2];
|
|
171
|
+
}
|
|
172
|
+
return env;
|
|
173
|
+
}
|
|
174
|
+
return void 0;
|
|
142
175
|
}
|
|
143
176
|
function codexBlockRange(lines) {
|
|
144
177
|
const start = lines.findIndex((l) => l.trim() === CODEX_HEADER);
|
|
@@ -184,13 +217,13 @@ function codexAdapter(configPath) {
|
|
|
184
217
|
const block = codexBlockText(ctx.entry);
|
|
185
218
|
if (range) {
|
|
186
219
|
const existingCommand = extractTomlCommand(lines.slice(range.start, range.end));
|
|
187
|
-
if (!commandPathIsStale(existingCommand)) {
|
|
220
|
+
if (!commandPathIsStale(existingCommand) && entryEnvMatches({ env: extractTomlEnv(lines.slice(range.start, range.end)) }, entryEnv(ctx.entry))) {
|
|
188
221
|
return { status: "already-configured", message: "\u5DF2\u914D\u7F6E\uFF08npx @latest \u81EA\u52A8\u66F4\u65B0\uFF09" };
|
|
189
222
|
}
|
|
190
223
|
const rebuilt = [...lines.slice(0, range.start), ...block.split("\n"), ...lines.slice(range.end)].join("\n");
|
|
191
224
|
await mkdir(dirname(configPath), { recursive: true });
|
|
192
225
|
await writeFile(configPath, rebuilt, "utf8");
|
|
193
|
-
return { status: "configured", message: "\u5DF2\u4FEE\u590D\
|
|
226
|
+
return { status: "configured", message: "\u5DF2\u4FEE\u590D\u542F\u52A8\u8DEF\u5F84/env\uFF08npx @latest\uFF09" };
|
|
194
227
|
}
|
|
195
228
|
await mkdir(dirname(configPath), { recursive: true });
|
|
196
229
|
await writeFile(configPath, `${content}${content && !content.endsWith("\n") ? "\n" : ""}
|
|
@@ -297,13 +330,16 @@ function zedAdapter(configPath) {
|
|
|
297
330
|
const existing = servers[SERVER_NAME];
|
|
298
331
|
if (existing && entryAutoUpdates(existing)) {
|
|
299
332
|
const path = isJsonObject(existing) && isJsonObject(existing.command) && typeof existing.command.path === "string" ? existing.command.path : void 0;
|
|
300
|
-
|
|
333
|
+
const existingCommand = isJsonObject(existing) && isJsonObject(existing.command) ? existing.command : void 0;
|
|
334
|
+
if (!commandPathIsStale(path) && entryEnvMatches(existingCommand, entryEnv(ctx.entry))) {
|
|
301
335
|
return { status: "already-configured", message: "\u5DF2\u914D\u7F6E\uFF08npx @latest \u81EA\u52A8\u66F4\u65B0\uFF09" };
|
|
302
336
|
}
|
|
303
337
|
}
|
|
338
|
+
const env = entryEnv(ctx.entry);
|
|
339
|
+
const commandObj = env ? { path: ctx.entry.command, args: ctx.entry.args, env } : { path: ctx.entry.command, args: ctx.entry.args };
|
|
304
340
|
config.context_servers = {
|
|
305
341
|
...servers,
|
|
306
|
-
[SERVER_NAME]: { command:
|
|
342
|
+
[SERVER_NAME]: { command: commandObj }
|
|
307
343
|
};
|
|
308
344
|
await mkdir(dirname(configPath), { recursive: true });
|
|
309
345
|
await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
@@ -370,7 +406,10 @@ function detectClients() {
|
|
|
370
406
|
label: "VS Code",
|
|
371
407
|
configPath: p.vsCode,
|
|
372
408
|
rootKey: "servers",
|
|
373
|
-
toEntry: (e) =>
|
|
409
|
+
toEntry: (e) => {
|
|
410
|
+
const env = entryEnv(e);
|
|
411
|
+
return env ? { type: "stdio", command: e.command, args: e.args, env } : { type: "stdio", command: e.command, args: e.args };
|
|
412
|
+
},
|
|
374
413
|
configuredMessage: "\u5DF2\u5199\u5165 .vscode/mcp.json",
|
|
375
414
|
restartHint: RESTART_HINTS.vscode
|
|
376
415
|
}),
|
|
@@ -523,6 +562,11 @@ function printNextSteps(clients, login) {
|
|
|
523
562
|
}
|
|
524
563
|
async function runInstall(opts = {}) {
|
|
525
564
|
console.log("Recolx MCP installer\n");
|
|
565
|
+
if (opts.env) {
|
|
566
|
+
writeEnvConfig(opts.env);
|
|
567
|
+
console.log(`\u2192 \u73AF\u5883\u5DF2\u8BBE\u4E3A ${opts.env}\uFF08~/.recolx/config.json\uFF09
|
|
568
|
+
`);
|
|
569
|
+
}
|
|
526
570
|
const clients = detectClients();
|
|
527
571
|
console.log("\u68C0\u6D4B\u5230\u7684 AI \u5BA2\u6237\u7AEF\uFF1A");
|
|
528
572
|
for (const c of clients) {
|
|
@@ -594,7 +638,7 @@ async function runUninstall(opts = {}) {
|
|
|
594
638
|
}
|
|
595
639
|
}
|
|
596
640
|
console.log();
|
|
597
|
-
console.log("\u5DF2\u6E05\u7406\u5BA2\u6237\u7AEF\u914D\u7F6E\u3002token \u6587\u4EF6 ~/.recolx/tokens-mcp
|
|
641
|
+
console.log("\u5DF2\u6E05\u7406\u5BA2\u6237\u7AEF\u914D\u7F6E\u3002token \u6587\u4EF6 ~/.recolx/tokens-mcp-*.json \u4FDD\u7559\uFF08\u5982\u9700\u5F7B\u5E95\u79FB\u9664\u8BF7\u5220\u9664\u8BE5\u76EE\u5F55\uFF09\u3002");
|
|
598
642
|
}
|
|
599
643
|
export {
|
|
600
644
|
CALLBACK_PORT,
|
|
@@ -604,8 +648,10 @@ export {
|
|
|
604
648
|
codexBlockRange,
|
|
605
649
|
codexBlockText,
|
|
606
650
|
commandPathIsStale,
|
|
651
|
+
extractTomlEnv,
|
|
607
652
|
getMcpEntry,
|
|
608
653
|
normalizeWindowsPath,
|
|
654
|
+
parseEnvFlag,
|
|
609
655
|
runInstall,
|
|
610
656
|
runUninstall
|
|
611
657
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uzqw/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Recolx read-only MCP server (stdio) — 7 read-only tools, readOnlyHint: true",
|
|
5
5
|
"mcpName": "io.github.uzqw/recolx-mcp",
|
|
6
6
|
"publishConfig": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"bin": {
|
|
11
|
-
"recolx-mcp": "
|
|
11
|
+
"recolx-mcp": "dist/index.js"
|
|
12
12
|
},
|
|
13
13
|
"main": "./dist/index.js",
|
|
14
14
|
"types": "./dist/index.d.ts",
|
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
"import": "./dist/index.js"
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
|
-
"files": [
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"skills"
|
|
24
|
+
],
|
|
22
25
|
"scripts": {
|
|
23
26
|
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
24
27
|
"test": "tsx --test test/*.test.ts"
|