@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-VFWABBBV.js");
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
- if (sub === "install") await runInstall({ yes, noLogin });
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
- if (isLocalPackageSpec(packageSpec)) {
34
- return { command: findNodeBinCommand("npm"), args: ["exec", "--yes", "--package", packageSpec, "--", "recolx-mcp"] };
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) => ({ command: e.command, args: e.args }));
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\u5931\u6548\u542F\u52A8\u8DEF\u5F84\uFF08npx @latest\uFF09" };
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
- if (!commandPathIsStale(path)) {
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: { path: ctx.entry.command, args: ctx.entry.args } }
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) => ({ type: "stdio", command: e.command, args: e.args }),
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.json \u4FDD\u7559\uFF08\u5982\u9700\u5F7B\u5E95\u79FB\u9664\u8BF7\u5220\u9664\u8BE5\u6587\u4EF6\uFF09\u3002");
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.0",
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": "./dist/index.js"
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": ["dist", "skills"],
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"