cwtools-mcp 0.1.0

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 ADDED
@@ -0,0 +1,279 @@
1
+ # cwtools-mcp
2
+
3
+ [English](#english) | [中文](#zh-cn)
4
+
5
+ <a id="english"></a>
6
+
7
+ ## English
8
+
9
+ CWTools MCP is the external-agent entry point for this extension's Paradox /
10
+ Stellaris semantic tools.
11
+
12
+ ### Default Mode: Extension Bridge
13
+
14
+ By default, `cwtools-mcp` is a lightweight MCP proxy. It does **not** start a
15
+ second `CWTools Server` process. Instead, it connects to the MCP bridge started
16
+ inside the active VS Code-compatible extension host and reuses that host's:
17
+
18
+ - existing CWTools language client;
19
+ - current workspace root;
20
+ - Problems diagnostics from the IDE;
21
+ - rules/cache/localisation/user settings;
22
+ - shared indexes and AI read tools.
23
+
24
+ This keeps memory use low and makes MCP diagnostics match the IDE.
25
+
26
+ Bridge mode is deliberately strict about project identity without requiring a
27
+ project path in global MCP settings. By default, the proxy discovers the current
28
+ client workspace from MCP roots, known per-session environment variables, or the
29
+ MCP process cwd. That workspace must match the `workspaceRoot` served by the
30
+ extension bridge. If they do not match, tool calls return `bridge_unavailable`
31
+ instead of silently answering from a different project. `--workspace` remains an
32
+ optional override for clients that cannot expose a per-project root.
33
+
34
+ The extension writes both files below into the current host's own
35
+ `globalStorage/mcp/` directory when the project is active:
36
+
37
+ ```text
38
+ cwtools-mcp.cjs
39
+ bridge-manifest.json
40
+ ```
41
+
42
+ External agents should run the `cwtools-mcp.cjs` copied by the same host they are
43
+ using. The proxy reads `bridge-manifest.json` next to itself. This is host-name
44
+ agnostic: VS Code, Cursor, VSCodium, Antigravity, and other compatible hosts all
45
+ work as long as they support the VS Code extension APIs and activate this
46
+ extension.
47
+
48
+ ### Quick Setup
49
+
50
+ Use the `globalStorage` path from the compatible host where the extension is
51
+ active. The placeholder below means the directory that contains the host's
52
+ `foreverskywalker.foreverskywalker-stellaris-cwtools` global storage folder.
53
+
54
+ #### Codex
55
+
56
+ ```sh
57
+ codex mcp add cwtools -- node "<host-globalStorage>/foreverskywalker.foreverskywalker-stellaris-cwtools/mcp/cwtools-mcp.cjs" --stdio
58
+ ```
59
+
60
+ #### Claude Code
61
+
62
+ ```sh
63
+ claude mcp add cwtools --scope user -- node "<host-globalStorage>/foreverskywalker.foreverskywalker-stellaris-cwtools/mcp/cwtools-mcp.cjs" --stdio
64
+ ```
65
+
66
+ #### Antigravity
67
+
68
+ Antigravity reads MCP servers from `~/.gemini/config/mcp_config.json`. Add this
69
+ server entry:
70
+
71
+ ```json
72
+ {
73
+ "mcpServers": {
74
+ "cwtools": {
75
+ "command": "node",
76
+ "args": [
77
+ "<host-globalStorage>/foreverskywalker.foreverskywalker-stellaris-cwtools/mcp/cwtools-mcp.cjs",
78
+ "--stdio"
79
+ ]
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ Or merge it into an existing config with a Node one-liner:
86
+
87
+ ```sh
88
+ node -e "const fs=require('fs'),os=require('os'),path=require('path');const p=path.join(os.homedir(),'.gemini','config','mcp_config.json');const s=process.argv[1];const cfg=fs.existsSync(p)?JSON.parse(fs.readFileSync(p,'utf8')):{};cfg.mcpServers={...(cfg.mcpServers||{}),cwtools:{command:'node',args:[s,'--stdio']}};fs.mkdirSync(path.dirname(p),{recursive:true});fs.writeFileSync(p,JSON.stringify(cfg,null,2)+'\n')" "<host-globalStorage>/foreverskywalker.foreverskywalker-stellaris-cwtools/mcp/cwtools-mcp.cjs"
89
+ ```
90
+
91
+ If the compatible host is closed, the workspace is not active, the manifest is
92
+ stale, or the client workspace differs from the bridge workspace, tool
93
+ calls return `bridge_unavailable` with recovery instructions. The proxy
94
+ intentionally does not silently fall back to a separate language server.
95
+
96
+ ### Optional Standalone Mode
97
+
98
+ Use standalone mode only when you explicitly want the legacy behavior: the MCP
99
+ process starts its own CWTools language server and builds its own diagnostic
100
+ state.
101
+
102
+ ```sh
103
+ cwtools-mcp --standalone --workspace /path/to/mod --game stellaris --stdio
104
+ ```
105
+
106
+ HTTP transport is available in both modes:
107
+
108
+ ```sh
109
+ cwtools-mcp --http --host 127.0.0.1 --port 3000
110
+ cwtools-mcp --standalone --workspace /path/to/mod --http --host 127.0.0.1 --port 3000
111
+ ```
112
+
113
+ ### Standalone Rules And Vanilla Cache
114
+
115
+ These options apply to standalone mode:
116
+
117
+ ```sh
118
+ cwtools-mcp --standalone --workspace /path/to/mod --game stellaris --game-path "/path/to/Stellaris"
119
+ cwtools-mcp --standalone --workspace /path/to/mod --game stellaris --cache "/path/to/.cwtools"
120
+ cwtools-mcp --standalone --workspace /path/to/mod --game stellaris --rules "/path/to/rules/config"
121
+ ```
122
+
123
+ When standalone mode lacks a vanilla `.cwb` cache or `--game-path`, vanilla IDs
124
+ may be absent and diagnostics can differ from the IDE. Bridge mode avoids this by
125
+ using the active extension host's loaded state.
126
+
127
+ ### Tools
128
+
129
+ The MCP surface remains read-only. It exposes the generated CWTools semantic
130
+ tools such as:
131
+
132
+ - `query_types`
133
+ - `query_rules`
134
+ - `query_scope`
135
+ - `get_diagnostics`
136
+ - `explore_pdx_project` for a bounded live semantic graph with dependency edges and freshness
137
+ - `query_workspace_index`
138
+ - `query_localisation_index`
139
+ - `get_pdx_block`
140
+ - completion, document/workspace symbols, definition and reference lookup
141
+ - deep semantic queries for scripted effects/triggers, enums, static modifiers,
142
+ variables, and entity info
143
+
144
+ File edits are intentionally not exposed through this MCP server. External agents
145
+ should edit files through their own environment and then call MCP diagnostics or
146
+ semantic tools again.
147
+
148
+ ---
149
+
150
+ <a id="zh-cn"></a>
151
+
152
+ ## 中文
153
+
154
+ CWTools MCP 是本扩展提供给外部 Agent 的 Paradox / Stellaris 语义工具入口。
155
+
156
+ ### 默认模式:插件内 Bridge
157
+
158
+ 默认情况下,`cwtools-mcp` 是一个轻量 MCP 代理。它**不会**再启动第二个
159
+ `CWTools Server` 进程,而是连接当前已激活的 VS Code 兼容宿主内的 MCP
160
+ bridge,并复用该宿主中的:
161
+
162
+ - 已有 CWTools 语言客户端;
163
+ - 当前工作区根目录;
164
+ - IDE Problems 面板诊断;
165
+ - rules/cache/localisation/用户设置;
166
+ - 共享索引和 AI 只读工具。
167
+
168
+ 这样可以降低内存占用,并让 MCP 诊断数量与 IDE 保持一致。
169
+
170
+ Bridge 模式会严格校验项目身份,但不要求把项目路径写死到全局 MCP 设置里。默认情况下,
171
+ 代理会从 MCP roots、已知的按会话注入的环境变量或 MCP 进程 cwd 推断当前客户端工作区。
172
+ 这个工作区必须与扩展 bridge 暴露的 `workspaceRoot` 一致。不一致时工具调用会返回
173
+ `bridge_unavailable`,不会静默使用另一个项目回答。`--workspace` 只保留给无法暴露
174
+ 按项目 root 的客户端作为可选覆盖项。
175
+
176
+ 项目激活时,扩展会把下面两个文件写入当前宿主自己的 `globalStorage/mcp/`
177
+ 目录:
178
+
179
+ ```text
180
+ cwtools-mcp.cjs
181
+ bridge-manifest.json
182
+ ```
183
+
184
+ 外部 Agent 应运行同一个宿主复制出来的 `cwtools-mcp.cjs`。代理会读取同目录的
185
+ `bridge-manifest.json`。这条主路径不依赖宿主目录名:VS Code、Cursor、
186
+ VSCodium、Antigravity,以及其他兼容 VS Code 扩展 API 的宿主都可以使用。
187
+
188
+ ### 快速接入
189
+
190
+ 请使用实际运行扩展的兼容宿主自己的 `globalStorage` 路径。下面的
191
+ `<host-globalStorage>` 代表该宿主下
192
+ `foreverskywalker.foreverskywalker-stellaris-cwtools` 全局存储目录所在位置。
193
+
194
+ #### Codex
195
+
196
+ ```sh
197
+ codex mcp add cwtools -- node "<host-globalStorage>/foreverskywalker.foreverskywalker-stellaris-cwtools/mcp/cwtools-mcp.cjs" --stdio
198
+ ```
199
+
200
+ #### Claude Code
201
+
202
+ ```sh
203
+ claude mcp add cwtools --scope user -- node "<host-globalStorage>/foreverskywalker.foreverskywalker-stellaris-cwtools/mcp/cwtools-mcp.cjs" --stdio
204
+ ```
205
+
206
+ #### Antigravity
207
+
208
+ Antigravity 从 `~/.gemini/config/mcp_config.json` 读取 MCP 服务器。添加下面这个
209
+ server 条目:
210
+
211
+ ```json
212
+ {
213
+ "mcpServers": {
214
+ "cwtools": {
215
+ "command": "node",
216
+ "args": [
217
+ "<host-globalStorage>/foreverskywalker.foreverskywalker-stellaris-cwtools/mcp/cwtools-mcp.cjs",
218
+ "--stdio"
219
+ ]
220
+ }
221
+ }
222
+ }
223
+ ```
224
+
225
+ 也可以用下面的 Node 一行命令合并进已有配置,不会覆盖其他 MCP server:
226
+
227
+ ```sh
228
+ node -e "const fs=require('fs'),os=require('os'),path=require('path');const p=path.join(os.homedir(),'.gemini','config','mcp_config.json');const s=process.argv[1];const cfg=fs.existsSync(p)?JSON.parse(fs.readFileSync(p,'utf8')):{};cfg.mcpServers={...(cfg.mcpServers||{}),cwtools:{command:'node',args:[s,'--stdio']}};fs.mkdirSync(path.dirname(p),{recursive:true});fs.writeFileSync(p,JSON.stringify(cfg,null,2)+'\n')" "<host-globalStorage>/foreverskywalker.foreverskywalker-stellaris-cwtools/mcp/cwtools-mcp.cjs"
229
+ ```
230
+
231
+ 如果兼容宿主未打开、工作区未激活、manifest 已失效,或客户端工作区与 bridge
232
+ 工作区不一致,工具调用会返回 `bridge_unavailable` 和恢复说明。代理不会静默回退并启动单独的语言服务。
233
+
234
+ ### 可选 Standalone 模式
235
+
236
+ 只有在明确需要旧行为时才使用 standalone 模式:MCP 进程会自行启动一份 CWTools
237
+ 语言服务器并构建独立诊断状态。
238
+
239
+ ```sh
240
+ cwtools-mcp --standalone --workspace /path/to/mod --game stellaris --stdio
241
+ ```
242
+
243
+ 两种模式都支持 HTTP transport:
244
+
245
+ ```sh
246
+ cwtools-mcp --http --host 127.0.0.1 --port 3000
247
+ cwtools-mcp --standalone --workspace /path/to/mod --http --host 127.0.0.1 --port 3000
248
+ ```
249
+
250
+ ### Standalone 的规则与原版缓存
251
+
252
+ 下面这些参数只适用于 standalone 模式:
253
+
254
+ ```sh
255
+ cwtools-mcp --standalone --workspace /path/to/mod --game stellaris --game-path "/path/to/Stellaris"
256
+ cwtools-mcp --standalone --workspace /path/to/mod --game stellaris --cache "/path/to/.cwtools"
257
+ cwtools-mcp --standalone --workspace /path/to/mod --game stellaris --rules "/path/to/rules/config"
258
+ ```
259
+
260
+ 如果 standalone 模式缺少 vanilla `.cwb` 缓存或 `--game-path`,原版 ID 可能缺失,
261
+ 诊断也可能与 IDE 不一致。Bridge 模式通过复用当前扩展宿主的已加载状态来避免这个问题。
262
+
263
+ ### 工具
264
+
265
+ MCP 入口仍保持只读。它暴露生成出来的 CWTools 语义工具,例如:
266
+
267
+ - `query_types`
268
+ - `query_rules`
269
+ - `query_scope`
270
+ - `get_diagnostics`
271
+ - `explore_pdx_project`:返回带依赖边与 freshness 的有界 live 语义图
272
+ - `query_workspace_index`
273
+ - `query_localisation_index`
274
+ - `get_pdx_block`
275
+ - 补全、document/workspace symbols、定义和引用查询
276
+ - scripted effects/triggers、enums、static modifiers、variables、entity info 等深层语义查询
277
+
278
+ 文件写入不会通过这个 MCP server 暴露。外部 Agent 应使用自己的环境编辑文件,
279
+ 然后再调用 MCP 诊断或语义工具复查。
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const config_1 = require("./config");
5
+ const nodeHostServices_1 = require("./hosts/nodeHostServices");
6
+ const bridgeProxy_1 = require("./mcp/bridgeProxy");
7
+ const transportHttp_1 = require("./mcp/transportHttp");
8
+ const transportStdio_1 = require("./mcp/transportStdio");
9
+ const server_1 = require("./server");
10
+ // Tear down host-owned resources (the spawned CWTools Server child) exactly
11
+ // once, then exit. MCP clients like Codex disconnect by closing the stdio pipe
12
+ // rather than asking us to shut down; without this the LSP child's pipes keep
13
+ // the event loop alive and orphan a multi-GB server process. host.dispose()
14
+ // only kills the child THIS process spawned, never any other instance.
15
+ function installLifecycle(host, stdio) {
16
+ let disposed = false;
17
+ const dispose = () => {
18
+ if (disposed)
19
+ return;
20
+ disposed = true;
21
+ try {
22
+ host.dispose?.();
23
+ }
24
+ catch {
25
+ // best-effort cleanup
26
+ }
27
+ };
28
+ const shutdown = () => {
29
+ dispose();
30
+ process.exit(0);
31
+ };
32
+ // Synchronous safety net for any other exit path.
33
+ process.once('exit', dispose);
34
+ process.once('SIGINT', shutdown);
35
+ process.once('SIGTERM', shutdown);
36
+ if (process.platform !== 'win32')
37
+ process.once('SIGHUP', shutdown);
38
+ // In stdio mode the client closing the pipe (EOF on our stdin) is the
39
+ // disconnect signal — shut down instead of lingering.
40
+ if (stdio) {
41
+ process.stdin.once('end', shutdown);
42
+ process.stdin.once('close', shutdown);
43
+ }
44
+ }
45
+ async function main() {
46
+ const config = (0, config_1.parseCliArgs)(process.argv.slice(2));
47
+ if (!config.standalone) {
48
+ if (config.http) {
49
+ await (0, transportHttp_1.runHttpTransport)(() => (0, bridgeProxy_1.createBridgeProxyMcpServer)(config), { host: config.host, port: config.port });
50
+ }
51
+ else if (config.stdio) {
52
+ const server = (0, bridgeProxy_1.createBridgeProxyMcpServer)(config);
53
+ await (0, transportStdio_1.runStdioTransport)(server);
54
+ }
55
+ else {
56
+ throw new Error('No MCP transport selected. Use --stdio or --http.');
57
+ }
58
+ return;
59
+ }
60
+ const host = (0, nodeHostServices_1.createNodeHostServices)(config);
61
+ installLifecycle(host, !!config.stdio && !config.http);
62
+ if (config.http) {
63
+ await (0, transportHttp_1.runHttpTransport)(() => (0, server_1.createCwtoolsMcpServer)(host), { host: config.host, port: config.port });
64
+ }
65
+ else if (config.stdio) {
66
+ const server = (0, server_1.createCwtoolsMcpServer)(host);
67
+ await (0, transportStdio_1.runStdioTransport)(server);
68
+ }
69
+ else {
70
+ throw new Error('No MCP transport selected. Use --stdio or --http.');
71
+ }
72
+ }
73
+ main().catch(error => {
74
+ console.error(error instanceof Error ? error.message : String(error));
75
+ process.exit(1);
76
+ });
@@ -0,0 +1,20 @@
1
+ export interface CwtoolsMcpConfig {
2
+ workspaceRoot: string;
3
+ game?: string;
4
+ serverPath?: string;
5
+ gamePath?: string;
6
+ cachePath?: string;
7
+ rulesPath?: string;
8
+ bridgeManifestPath?: string;
9
+ stdio: boolean;
10
+ http: boolean;
11
+ host: string;
12
+ port: number;
13
+ enableWrites: boolean;
14
+ allowedTools: string[];
15
+ forceStart: boolean;
16
+ standalone: boolean;
17
+ workspaceRootExplicit: boolean;
18
+ }
19
+ export declare function parseCliArgs(argv: string[]): CwtoolsMcpConfig;
20
+ export declare function helpText(): string;
package/dist/config.js ADDED
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.parseCliArgs = parseCliArgs;
37
+ exports.helpText = helpText;
38
+ const path = __importStar(require("path"));
39
+ function parseCliArgs(argv) {
40
+ const config = {
41
+ workspaceRoot: process.cwd(),
42
+ stdio: true,
43
+ http: false,
44
+ host: '127.0.0.1',
45
+ port: 3000,
46
+ enableWrites: false,
47
+ allowedTools: [],
48
+ forceStart: false,
49
+ standalone: false,
50
+ workspaceRootExplicit: false,
51
+ };
52
+ for (let index = 0; index < argv.length; index++) {
53
+ const arg = argv[index];
54
+ switch (arg) {
55
+ case '--workspace':
56
+ config.workspaceRoot = path.resolve(readValue(argv, ++index, arg));
57
+ config.workspaceRootExplicit = true;
58
+ break;
59
+ case '--game':
60
+ config.game = readValue(argv, ++index, arg);
61
+ break;
62
+ case '--server-path':
63
+ config.serverPath = path.resolve(readValue(argv, ++index, arg));
64
+ break;
65
+ case '--game-path':
66
+ config.gamePath = path.resolve(readValue(argv, ++index, arg));
67
+ break;
68
+ case '--cache':
69
+ config.cachePath = path.resolve(readValue(argv, ++index, arg));
70
+ break;
71
+ case '--rules':
72
+ config.rulesPath = resolveRulesPath(readValue(argv, ++index, arg));
73
+ break;
74
+ case '--bridge-manifest':
75
+ config.bridgeManifestPath = path.resolve(readValue(argv, ++index, arg));
76
+ break;
77
+ case '--stdio':
78
+ config.stdio = true;
79
+ config.http = false;
80
+ break;
81
+ case '--http':
82
+ config.http = true;
83
+ config.stdio = false;
84
+ break;
85
+ case '--host':
86
+ config.host = readValue(argv, ++index, arg);
87
+ break;
88
+ case '--port':
89
+ config.port = readPort(readValue(argv, ++index, arg));
90
+ break;
91
+ case '--enable-writes':
92
+ config.enableWrites = true;
93
+ break;
94
+ case '--force-start':
95
+ config.forceStart = true;
96
+ break;
97
+ case '--standalone':
98
+ config.standalone = true;
99
+ break;
100
+ case '--allow-tool':
101
+ config.allowedTools.push(readValue(argv, ++index, arg));
102
+ break;
103
+ case '--help':
104
+ case '-h':
105
+ throw new Error(helpText());
106
+ default:
107
+ if (arg?.startsWith('--allow-tool=')) {
108
+ config.allowedTools.push(arg.slice('--allow-tool='.length));
109
+ }
110
+ else if (arg?.startsWith('--workspace=')) {
111
+ config.workspaceRoot = path.resolve(arg.slice('--workspace='.length));
112
+ config.workspaceRootExplicit = true;
113
+ }
114
+ else if (arg?.startsWith('--game=')) {
115
+ config.game = arg.slice('--game='.length);
116
+ }
117
+ else if (arg?.startsWith('--server-path=')) {
118
+ config.serverPath = path.resolve(arg.slice('--server-path='.length));
119
+ }
120
+ else if (arg?.startsWith('--game-path=')) {
121
+ config.gamePath = path.resolve(arg.slice('--game-path='.length));
122
+ }
123
+ else if (arg?.startsWith('--cache=')) {
124
+ config.cachePath = path.resolve(arg.slice('--cache='.length));
125
+ }
126
+ else if (arg?.startsWith('--rules=')) {
127
+ config.rulesPath = resolveRulesPath(arg.slice('--rules='.length));
128
+ }
129
+ else if (arg?.startsWith('--bridge-manifest=')) {
130
+ config.bridgeManifestPath = path.resolve(arg.slice('--bridge-manifest='.length));
131
+ }
132
+ else if (arg?.startsWith('--host=')) {
133
+ config.host = arg.slice('--host='.length);
134
+ }
135
+ else if (arg?.startsWith('--port=')) {
136
+ config.port = readPort(arg.slice('--port='.length));
137
+ }
138
+ else if (arg === '--http') {
139
+ config.http = true;
140
+ config.stdio = false;
141
+ }
142
+ else if (arg === '--standalone') {
143
+ config.standalone = true;
144
+ }
145
+ else {
146
+ throw new Error(`Unknown argument: ${arg}\n\n${helpText()}`);
147
+ }
148
+ }
149
+ }
150
+ return config;
151
+ }
152
+ function resolveRulesPath(value) {
153
+ const resolved = path.resolve(value);
154
+ if (resolved.toLowerCase().endsWith('.zip')) {
155
+ throw new Error('--rules must be a directory, not a .zip archive');
156
+ }
157
+ return resolved;
158
+ }
159
+ function readPort(value) {
160
+ const port = Number(value);
161
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
162
+ throw new Error(`Invalid --port value: ${value}`);
163
+ }
164
+ return port;
165
+ }
166
+ function readValue(argv, index, flag) {
167
+ const value = argv[index];
168
+ if (!value || value.startsWith('--')) {
169
+ throw new Error(`Missing value for ${flag}`);
170
+ }
171
+ return value;
172
+ }
173
+ function helpText() {
174
+ return [
175
+ 'Usage:',
176
+ ' cwtools-mcp [--stdio] [--bridge-manifest <path>] [--workspace <path>]',
177
+ ' cwtools-mcp --http [--host 127.0.0.1] [--port 3000] [--bridge-manifest <path>] [--workspace <path>]',
178
+ ' cwtools-mcp --standalone --workspace <path> [--game stellaris] [--stdio] [--server-path <path>]',
179
+ ' cwtools-mcp --standalone --workspace <path> --http [--host 127.0.0.1] [--port 3000]',
180
+ ' cwtools-mcp --standalone --workspace <path> --enable-writes',
181
+ ' cwtools-mcp --standalone --workspace <path> --enable-writes --allow-tool write_localisation',
182
+ '',
183
+ 'Default bridge mode:',
184
+ ' The script connects to the extension-host MCP bridge written by the active',
185
+ ' VS Code-compatible host. The manifest (bridge-manifest.json) is looked up next',
186
+ ' to this script first, then auto-detected under the host globalStorage of VS Code,',
187
+ ' Insiders, VSCodium, Cursor, and Antigravity. It does not start a second CWTools',
188
+ ' language server. The client workspace (MCP roots, environment workspace, cwd,',
189
+ ' or --workspace when supplied) must match the bridge workspace exactly. If the',
190
+ ' compatible host is closed or the workspace does not match, tool calls return an',
191
+ ' actionable unavailable error.',
192
+ ' Use --standalone only when you intentionally want the legacy self-hosted LSP mode.',
193
+ '',
194
+ 'Vanilla data (needed for vanilla IDs and correct mod-vs-vanilla diagnostics):',
195
+ ' (auto) If neither flag is given, the VS Code cwtools extension cache',
196
+ ' in globalStorage is auto-detected and reused.',
197
+ ' --game-path <dir> Vanilla install/data dir; the server builds the cache from it (slow first run).',
198
+ ' --cache <dir> Dir holding a pre-built <game>.cwb cache (overrides auto-detection),',
199
+ ' loaded directly instead of rebuilding. Without any, results are mod-only.',
200
+ '',
201
+ 'Rules source (priority: --rules > installed extension > dev checkout):',
202
+ ' --rules <dir> Explicit CWT rules directory (a .zip is rejected). When omitted, the',
203
+ ' rules the installed VS Code extension pulled into globalStorage are used.',
204
+ '',
205
+ 'Project gate:',
206
+ ' (default) Tools are always listed. The language server starts only when the',
207
+ ' workspace is (or sits in/above) a Paradox mod (descriptor.mod, common/,',
208
+ ' events/, localisation/, .cwtools/). On other workspaces tool calls are',
209
+ ' rejected with a reason and no server spawns.',
210
+ ' --force-start Treat the workspace as supported even without mod markers.',
211
+ ].join('\n');
212
+ }