@qodalis/mcp-cli 0.0.0-stage → 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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +273 -2
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +92 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.d.ts +56 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +130 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/env-file.d.ts +9 -0
- package/dist/src/env-file.d.ts.map +1 -0
- package/dist/src/env-file.js +54 -0
- package/dist/src/env-file.js.map +1 -0
- package/dist/src/errors.d.ts +22 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +115 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/filters.d.ts +13 -0
- package/dist/src/filters.d.ts.map +1 -0
- package/dist/src/filters.js +16 -0
- package/dist/src/filters.js.map +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +7 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/remote.d.ts +62 -0
- package/dist/src/remote.d.ts.map +1 -0
- package/dist/src/remote.js +235 -0
- package/dist/src/remote.js.map +1 -0
- package/dist/src/server.d.ts +13 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +86 -0
- package/dist/src/server.js.map +1 -0
- package/package.json +67 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-10-15
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- First release: a stdio MCP server that proxies Claude Code (or any MCP client)
|
|
12
|
+
to a Qodalis CLI server's MCP endpoint (`<QCLI_URL>/api/qcli/mcp`). Tools are
|
|
13
|
+
taken from the server's `tools/list` and passed through unchanged; calls are
|
|
14
|
+
forwarded.
|
|
15
|
+
- Authentication with an API token or the server's static MCP token
|
|
16
|
+
(`QCLI_TOKEN`), or with `QCLI_USER` / `QCLI_PASSWORD` (signs in again after a
|
|
17
|
+
401).
|
|
18
|
+
- `QCLI_READ_ONLY`, `QCLI_TOOLS` and `QCLI_EXCLUDE` client-side tool filters.
|
|
19
|
+
- Resilience: connect on first use, failed connects are not cached, one retry on
|
|
20
|
+
connection loss, fast failure (`QCLI_CONNECTION_TIMEOUT`) with actionable
|
|
21
|
+
messages for unreachable servers, rejected credentials, missing permissions and
|
|
22
|
+
MCP not enabled; 60 s tool-list cache refreshed for unknown tools.
|
|
23
|
+
- `QCLI_REQUEST_TIMEOUT`, `QCLI_TRUST_CERT`, `--env-file` / `QCLI_ENV_FILE`,
|
|
24
|
+
`QODALIS_CLI_*` aliases, `--help`, `--version`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Qodalis Solutions SRL
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,274 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @qodalis/mcp-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A resilient [Model Context Protocol](https://modelcontextprotocol.io) server that
|
|
4
|
+
gives [Claude Code](https://code.claude.com) a [Qodalis CLI](https://cli.qodalis.com)
|
|
5
|
+
server: its CLI commands, files, data explorer, jobs and plugin tools.
|
|
6
|
+
|
|
7
|
+
Every Qodalis CLI server (.NET, Node or Python) can expose an MCP endpoint at
|
|
8
|
+
`<server>/api/qcli/mcp`. `mcp-cli` runs on your machine as a stdio MCP server
|
|
9
|
+
and proxies to that endpoint: Claude Code sees exactly the server's tools, with
|
|
10
|
+
the permissions of the account you configure. It handles sign-in, reconnects
|
|
11
|
+
after network drops and lets you narrow what the assistant may use.
|
|
12
|
+
|
|
13
|
+
## Highlights
|
|
14
|
+
|
|
15
|
+
- **No tool-specific code.** Tools come from the server's `tools/list` and are
|
|
16
|
+
passed through unchanged (names, schemas, annotations); calls are forwarded.
|
|
17
|
+
New commands on the server show up without updating the proxy.
|
|
18
|
+
- **Token or password.** Use an API token (`QCLI_TOKEN`), or sign in with
|
|
19
|
+
`QCLI_USER` / `QCLI_PASSWORD`; an expired session is renewed automatically on
|
|
20
|
+
the next call.
|
|
21
|
+
- **Read-only mode** (`QCLI_READ_ONLY=true`) exposes only the tools the server
|
|
22
|
+
marks read-only, and refuses calls to anything else.
|
|
23
|
+
- **Client-side allow and deny lists** (`QCLI_TOOLS`, `QCLI_EXCLUDE`, trailing
|
|
24
|
+
`*` allowed) on top of the server's own permissions.
|
|
25
|
+
- **Self-healing connection.** It connects on first use, never caches a failed
|
|
26
|
+
connect, retries once when the connection drops, and fails fast (8 s) when the
|
|
27
|
+
server is unreachable, with a message that says what to check.
|
|
28
|
+
- **Clear errors**: a rejected token, MCP not enabled on the server, a missing
|
|
29
|
+
permission or an unreachable host each come back as one actionable sentence.
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx -y @qodalis/mcp-cli --help
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The server must have MCP enabled (`mcp` option; see
|
|
38
|
+
[MCP endpoint](https://cli.qodalis.com/docs/server-integration#mcp-endpoint)).
|
|
39
|
+
Default ports: .NET `8046`, Node `8047`, Python `8048`.
|
|
40
|
+
|
|
41
|
+
### Claude Code
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
claude mcp add qcli -- npx -y @qodalis/mcp-cli
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
then set the environment in `~/.claude.json`:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"mcpServers": {
|
|
52
|
+
"qcli": {
|
|
53
|
+
"command": "npx",
|
|
54
|
+
"args": ["-y", "@qodalis/mcp-cli"],
|
|
55
|
+
"env": {
|
|
56
|
+
"QCLI_URL": "http://localhost:8047",
|
|
57
|
+
"QCLI_TOKEN": "qcli_pat_..."
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
or sign in with a user name and password instead of a token:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
"env": {
|
|
68
|
+
"QCLI_URL": "http://localhost:8047",
|
|
69
|
+
"QCLI_USER": "ops",
|
|
70
|
+
"QCLI_PASSWORD": "********"
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The same in one command, with `-e` flags:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
claude mcp add qcli -e QCLI_URL=http://localhost:8047 -e QCLI_TOKEN=qcli_pat_... -- npx -y @qodalis/mcp-cli
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Add `--scope user` to make it available in every project. With
|
|
81
|
+
`--scope project` the entry goes into the project's `.mcp.json`, which is
|
|
82
|
+
usually committed: keep the secret out of it with
|
|
83
|
+
`"QCLI_TOKEN": "${QCLI_TOKEN}"` and set the variable in your shell. A `.env`
|
|
84
|
+
file works too:
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{ "command": "npx", "args": ["-y", "@qodalis/mcp-cli", "--env-file", "/path/to/.env"] }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Pin a version in shared configs (`@qodalis/mcp-cli@0.1.0`) so an upgrade is a
|
|
91
|
+
deliberate change.
|
|
92
|
+
|
|
93
|
+
### Getting a token
|
|
94
|
+
|
|
95
|
+
In the web CLI connected to the server (users plugin), signed in as the account
|
|
96
|
+
the assistant should act as:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
token create claude --expires=90
|
|
100
|
+
# token create claude --permissions=commands:execute,files:read (narrower scope)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The token (`qcli_pat_…`) is shown once. Without the web CLI, sign in and create
|
|
104
|
+
one with curl:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
SESSION=$(curl -s http://localhost:8047/api/v1/qcli/sessions \
|
|
108
|
+
-H 'Content-Type: application/json' -d '{"username":"ops","password":"..."}' | jq -r .token)
|
|
109
|
+
curl -s http://localhost:8047/api/v1/qcli/tokens -H "Authorization: Bearer $SESSION" \
|
|
110
|
+
-H 'Content-Type: application/json' -d '{"name":"claude","expiresInDays":90}' | jq -r .token
|
|
111
|
+
curl -s -X DELETE http://localhost:8047/api/v1/qcli/sessions/current -H "Authorization: Bearer $SESSION"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
A server **without** the users plugin protects its endpoint with a static token
|
|
115
|
+
(its `mcp.token` option): put that value in `QCLI_TOKEN`. If it sets none, the
|
|
116
|
+
endpoint is open and no auth variable is needed.
|
|
117
|
+
|
|
118
|
+
## Configuration
|
|
119
|
+
|
|
120
|
+
Every setting is an environment variable. `QODALIS_CLI_*` names are accepted as
|
|
121
|
+
aliases for every key.
|
|
122
|
+
|
|
123
|
+
| Variable | Default | Description |
|
|
124
|
+
| ------------------------- | -------- | -------------------------------------------------------------------------------------------------- |
|
|
125
|
+
| `QCLI_URL` | required | Server base URL, e.g. `https://cli.example.com` (a pasted `…/api/qcli/mcp` URL is accepted). |
|
|
126
|
+
| `QCLI_TOKEN` | none | API token (`token create` in the web CLI) or the server's static `mcp.token`. |
|
|
127
|
+
| `QCLI_USER` | none | Sign in with this user name instead of a token (needs `QCLI_PASSWORD`). |
|
|
128
|
+
| `QCLI_PASSWORD` | none | Password for `QCLI_USER`. Never logged. |
|
|
129
|
+
| `QCLI_READ_ONLY` | `false` | Expose and allow only tools annotated `readOnlyHint: true`. See [read-only mode](#read-only-mode). |
|
|
130
|
+
| `QCLI_TOOLS` | all | Comma-separated allow-list of tool names; a trailing `*` matches a prefix (`fs_*`). |
|
|
131
|
+
| `QCLI_EXCLUDE` | none | Comma-separated deny-list, same syntax, applied after `QCLI_TOOLS`. |
|
|
132
|
+
| `QCLI_CONNECTION_TIMEOUT` | `8000` | Milliseconds to wait for the TCP/TLS connection before reporting the server unreachable. |
|
|
133
|
+
| `QCLI_REQUEST_TIMEOUT` | `60000` | Milliseconds a single request (`tools/list`, `tools/call`) may take. |
|
|
134
|
+
| `QCLI_TRUST_CERT` | `false` | Accept a self-signed TLS certificate. |
|
|
135
|
+
| `QCLI_ENV_FILE` | none | Path to a `.env` file (same as `--env-file`). Variables already set in the environment win. |
|
|
136
|
+
|
|
137
|
+
Set either `QCLI_TOKEN` or `QCLI_USER` + `QCLI_PASSWORD`, not both.
|
|
138
|
+
|
|
139
|
+
## Tools
|
|
140
|
+
|
|
141
|
+
The tools are whatever the server exposes to your account. A typical server
|
|
142
|
+
offers:
|
|
143
|
+
|
|
144
|
+
| Tools | Available when |
|
|
145
|
+
| -------------------------------------------------------------------- | -------------------------------------- |
|
|
146
|
+
| `server_info` | always: version, enabled features |
|
|
147
|
+
| one tool per CLI command (`weather`, `weather_current`, ...) | always |
|
|
148
|
+
| `fs_list`, `fs_stat`, `fs_read`, `fs_write`, `fs_mkdir`, `fs_delete` | the server's filesystem API is enabled |
|
|
149
|
+
| `data_sources`, `data_schema`, `data_query` | the data explorer is enabled |
|
|
150
|
+
| `jobs_list`, `jobs_get`, `jobs_history`, `jobs_trigger`, ... | the jobs plugin is installed |
|
|
151
|
+
| plugin tools | a server plugin contributes them |
|
|
152
|
+
|
|
153
|
+
Each one is limited by the account's permissions on the server (for example
|
|
154
|
+
`commands:execute`, `files:read` / `files:write`, `data:query`, `jobs:manage`),
|
|
155
|
+
and by the token's scope when it was created with `--permissions`.
|
|
156
|
+
|
|
157
|
+
## Read-only mode
|
|
158
|
+
|
|
159
|
+
With `QCLI_READ_ONLY=true` the proxy lists only the tools the server annotates
|
|
160
|
+
`readOnlyHint: true` (such as `server_info`, `fs_list`, `fs_read`,
|
|
161
|
+
`data_schema`, `jobs_list`) and refuses calls to any other tool before they
|
|
162
|
+
reach the server. Command tools carry no read-only annotation, so they are
|
|
163
|
+
hidden in this mode; add the ones you want with a separate, non-read-only entry
|
|
164
|
+
and `QCLI_TOOLS`.
|
|
165
|
+
|
|
166
|
+
The filter is defence in depth. The real boundary is the account: give the
|
|
167
|
+
assistant a user (or a token scope) with only the permissions it should have.
|
|
168
|
+
|
|
169
|
+
## Several servers
|
|
170
|
+
|
|
171
|
+
Add one entry per server (and per access level) with a distinct name:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"mcpServers": {
|
|
176
|
+
"qcli-dev": {
|
|
177
|
+
"command": "npx",
|
|
178
|
+
"args": ["-y", "@qodalis/mcp-cli"],
|
|
179
|
+
"env": { "QCLI_URL": "http://localhost:8047", "QCLI_TOKEN": "qcli_pat_dev..." }
|
|
180
|
+
},
|
|
181
|
+
"qcli-prod": {
|
|
182
|
+
"command": "npx",
|
|
183
|
+
"args": ["-y", "@qodalis/mcp-cli"],
|
|
184
|
+
"env": {
|
|
185
|
+
"QCLI_URL": "https://cli.example.com",
|
|
186
|
+
"QCLI_TOKEN": "qcli_pat_prod...",
|
|
187
|
+
"QCLI_READ_ONLY": "true"
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
"qcli-prod-jobs": {
|
|
191
|
+
"command": "npx",
|
|
192
|
+
"args": ["-y", "@qodalis/mcp-cli"],
|
|
193
|
+
"env": {
|
|
194
|
+
"QCLI_URL": "https://cli.example.com",
|
|
195
|
+
"QCLI_TOKEN": "qcli_pat_prod...",
|
|
196
|
+
"QCLI_TOOLS": "jobs_*,server_info",
|
|
197
|
+
"QCLI_EXCLUDE": "jobs_cancel"
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Troubleshooting
|
|
205
|
+
|
|
206
|
+
`claude mcp list` shows whether each server connects; inside a session `/mcp`
|
|
207
|
+
shows its status and tools and lets you reconnect. The proxy logs to stderr
|
|
208
|
+
(`claude --debug` shows it).
|
|
209
|
+
|
|
210
|
+
| Message | What to do |
|
|
211
|
+
| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
212
|
+
| `… answered 401 (not authenticated): QCLI_TOKEN is wrong, expired or revoked` | Create a new token (`token create claude`) and update `QCLI_TOKEN`. `token list` in the web CLI shows expiry dates. |
|
|
213
|
+
| `… rejected QCLI_USER / QCLI_PASSWORD` | Check the credentials; the account may be disabled or locked out after failed sign-ins. |
|
|
214
|
+
| `… answered 403 …` | The account lacks `commands:execute`, or the server rejected the request origin. |
|
|
215
|
+
| `MCP is not enabled on this server (enable the mcp option)` | Turn on the server's `mcp` option and restart it; check `QCLI_URL` (base URL, not a page of the web app). |
|
|
216
|
+
| `Cannot reach … Check QCLI_URL / VPN.` | The server is down, the URL or port is wrong, or the VPN is disconnected. Retry the call once it is back: the proxy reconnects by itself. |
|
|
217
|
+
| `… did not answer within 60000 ms` | A slow command; raise `QCLI_REQUEST_TIMEOUT`. |
|
|
218
|
+
| A tool is missing | The account (or token scope) lacks its permission, the feature is not enabled on the server, or `QCLI_READ_ONLY` / `QCLI_TOOLS` / `QCLI_EXCLUDE` hide it. |
|
|
219
|
+
| `QCLI_URL … is required` at startup | The `env` block is missing or the variable name is misspelled; `npx -y @qodalis/mcp-cli --help` lists every variable. |
|
|
220
|
+
|
|
221
|
+
## How reconnection works
|
|
222
|
+
|
|
223
|
+
1. The proxy connects to the endpoint on the first `tools/list` or
|
|
224
|
+
`tools/call`, not at startup, so Claude Code starts even when the server is
|
|
225
|
+
down.
|
|
226
|
+
2. A failed connect is not cached: the next call tries again.
|
|
227
|
+
3. A connection-level error (refused, reset, socket closed, DNS failure) drops
|
|
228
|
+
the connection and retries the request once on a fresh one. A connect timeout
|
|
229
|
+
is not retried, so an unreachable host fails within `QCLI_CONNECTION_TIMEOUT`.
|
|
230
|
+
4. With `QCLI_USER` / `QCLI_PASSWORD`, a 401 signs in again and retries once.
|
|
231
|
+
5. The tool list is cached for 60 s; a call to a tool the proxy does not know
|
|
232
|
+
yet refreshes it first, so new server commands work without a restart.
|
|
233
|
+
|
|
234
|
+
## Security notes
|
|
235
|
+
|
|
236
|
+
- Tokens and passwords are read from the environment, sent only to `QCLI_URL`,
|
|
237
|
+
and never written to logs or tool results.
|
|
238
|
+
- Prefer an API token with a narrow `--permissions` scope and an expiry over a
|
|
239
|
+
password; revoke it with `token revoke <id>` when it is no longer needed.
|
|
240
|
+
- Use `https://` for anything but `localhost`. `QCLI_TRUST_CERT=true` disables
|
|
241
|
+
certificate verification for this process; use it only for self-signed
|
|
242
|
+
certificates you control.
|
|
243
|
+
- Nothing is written to stdout except the MCP protocol stream.
|
|
244
|
+
|
|
245
|
+
See [SECURITY.md](SECURITY.md) for reporting vulnerabilities.
|
|
246
|
+
|
|
247
|
+
## Library use
|
|
248
|
+
|
|
249
|
+
The pieces are exported for embedding:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
import { createProxyServer, loadConfig, RemoteServer } from '@qodalis/mcp-cli';
|
|
253
|
+
|
|
254
|
+
const config = loadConfig(process.env);
|
|
255
|
+
const remote = new RemoteServer(config, { clientInfo: { name: 'my-app', version: '1.0.0' } });
|
|
256
|
+
const server = createProxyServer(remote, config); // a low-level MCP Server: connect any transport
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Development
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
npm install
|
|
263
|
+
npm run build
|
|
264
|
+
npm test # unit tests and stdio end-to-end tests against a fake server (build first)
|
|
265
|
+
npm run typecheck
|
|
266
|
+
npm run check-format
|
|
267
|
+
npm run dev -- --env-file .env # run from source
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
271
|
+
|
|
272
|
+
## License
|
|
273
|
+
|
|
274
|
+
[MIT](LICENSE) © Qodalis Solutions SRL
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
3
|
+
import { ConfigError, describeConfig, ENV_FILE_VARIABLES, loadConfig } from './config.js';
|
|
4
|
+
import { applyEnvFile } from './env-file.js';
|
|
5
|
+
import { RemoteServer } from './remote.js';
|
|
6
|
+
import { createProxyServer, SERVER_NAME, SERVER_VERSION } from './server.js';
|
|
7
|
+
const SHUTDOWN_GRACE_MS = 5000;
|
|
8
|
+
const usage = `Proxies Claude Code (or any MCP client) to a Qodalis CLI server's MCP endpoint
|
|
9
|
+
(<QCLI_URL>/api/qcli/mcp). Configuration is read from the environment (QCLI_* or
|
|
10
|
+
QODALIS_CLI_* names):
|
|
11
|
+
QCLI_URL (required) server base URL, e.g. http://localhost:8047
|
|
12
|
+
QCLI_TOKEN API token (token create in the web CLI) or the server's mcp token
|
|
13
|
+
QCLI_USER, QCLI_PASSWORD sign in with a password instead (signs in again on 401)
|
|
14
|
+
QCLI_READ_ONLY (false) only tools the server marks read-only
|
|
15
|
+
QCLI_TOOLS comma-separated allow-list of tool names (trailing * allowed)
|
|
16
|
+
QCLI_EXCLUDE comma-separated deny-list, applied after QCLI_TOOLS
|
|
17
|
+
QCLI_CONNECTION_TIMEOUT (8000 ms) QCLI_REQUEST_TIMEOUT (60000 ms)
|
|
18
|
+
QCLI_TRUST_CERT (false) accept a self-signed TLS certificate
|
|
19
|
+
QCLI_ENV_FILE path to a .env file (same as --env-file)
|
|
20
|
+
`;
|
|
21
|
+
const log = (message) => {
|
|
22
|
+
// Anything written to stdout corrupts the JSON-RPC stream: all logging goes to stderr.
|
|
23
|
+
process.stderr.write(`[${SERVER_NAME}] ${message}\n`);
|
|
24
|
+
};
|
|
25
|
+
const parseArgs = (argv) => {
|
|
26
|
+
const result = {};
|
|
27
|
+
for (let i = 0; i < argv.length; i++) {
|
|
28
|
+
const arg = argv[i];
|
|
29
|
+
if (arg === '--help' || arg === '-h') {
|
|
30
|
+
process.stdout.write(`Usage: mcp-cli [--env-file <path>] [--help] [--version]\n\n${usage}`);
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
if (arg === '--version' || arg === '-v') {
|
|
34
|
+
process.stdout.write(`${SERVER_VERSION}\n`);
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
if (arg === '--env-file') {
|
|
38
|
+
const value = argv[i + 1];
|
|
39
|
+
if (!value) {
|
|
40
|
+
throw new ConfigError('--env-file requires a path.');
|
|
41
|
+
}
|
|
42
|
+
result.envFile = value;
|
|
43
|
+
i++;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
throw new ConfigError(`Unknown argument "${arg}".`);
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
const main = () => {
|
|
51
|
+
try {
|
|
52
|
+
const args = parseArgs(process.argv.slice(2));
|
|
53
|
+
const envFile = args.envFile ?? ENV_FILE_VARIABLES.map((name) => process.env[name]).find(Boolean);
|
|
54
|
+
if (envFile) {
|
|
55
|
+
applyEnvFile(envFile, process.env);
|
|
56
|
+
}
|
|
57
|
+
const config = loadConfig(process.env);
|
|
58
|
+
if (config.trustCert) {
|
|
59
|
+
log('QCLI_TRUST_CERT=true: TLS certificates are not verified');
|
|
60
|
+
}
|
|
61
|
+
const remote = new RemoteServer(config, {
|
|
62
|
+
clientInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
63
|
+
log,
|
|
64
|
+
});
|
|
65
|
+
const handle = serveStdio(() => createProxyServer(remote, config), {
|
|
66
|
+
onerror: (error) => log(`transport error: ${error.message}`),
|
|
67
|
+
});
|
|
68
|
+
log(`v${SERVER_VERSION} ready for ${describeConfig(config)}; connects on first use`);
|
|
69
|
+
let shuttingDown = false;
|
|
70
|
+
const shutdown = async (reason) => {
|
|
71
|
+
if (shuttingDown) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
shuttingDown = true;
|
|
75
|
+
log(`shutting down (${reason})`);
|
|
76
|
+
setTimeout(() => process.exit(0), SHUTDOWN_GRACE_MS).unref();
|
|
77
|
+
await handle.close().catch(() => undefined);
|
|
78
|
+
await remote.close();
|
|
79
|
+
process.exit(0);
|
|
80
|
+
};
|
|
81
|
+
process.once('SIGINT', () => void shutdown('SIGINT'));
|
|
82
|
+
process.once('SIGTERM', () => void shutdown('SIGTERM'));
|
|
83
|
+
process.stdin.once('end', () => void shutdown('stdin closed'));
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
87
|
+
log(`fatal: ${message}`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
main();
|
|
92
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment-driven configuration. Primary names use `QCLI_`; `QODALIS_CLI_`
|
|
3
|
+
* is accepted as an alias for every key.
|
|
4
|
+
*/
|
|
5
|
+
export type Env = Record<string, string | undefined>;
|
|
6
|
+
export declare class ConfigError extends Error {
|
|
7
|
+
constructor(message: string);
|
|
8
|
+
}
|
|
9
|
+
/** How the proxy authenticates to the Qodalis CLI server. */
|
|
10
|
+
export type AuthConfig = {
|
|
11
|
+
kind: 'none';
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'token';
|
|
14
|
+
token: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'password';
|
|
17
|
+
password: string;
|
|
18
|
+
user: string;
|
|
19
|
+
};
|
|
20
|
+
export type Config = {
|
|
21
|
+
auth: AuthConfig;
|
|
22
|
+
connectionTimeoutMs: number;
|
|
23
|
+
/** Client-side deny-list (trailing `*` allowed), applied after `tools` */
|
|
24
|
+
exclude: string[];
|
|
25
|
+
/** Expose and allow only tools annotated `readOnlyHint: true` */
|
|
26
|
+
readOnly: boolean;
|
|
27
|
+
requestTimeoutMs: number;
|
|
28
|
+
/** Client-side allow-list (trailing `*` allowed); empty = every tool */
|
|
29
|
+
tools: string[];
|
|
30
|
+
/** Accept self-signed TLS certificates */
|
|
31
|
+
trustCert: boolean;
|
|
32
|
+
/** Server base URL, e.g. `https://cli.example.com` (no trailing slash) */
|
|
33
|
+
url: string;
|
|
34
|
+
};
|
|
35
|
+
export declare const PREFIXES: string[];
|
|
36
|
+
/** `QCLI_<suffix>` plus the `QODALIS_CLI_<suffix>` alias. */
|
|
37
|
+
export declare const names: (suffix: string) => string[];
|
|
38
|
+
export declare const ENV_FILE_VARIABLES: string[];
|
|
39
|
+
export type EnvReader = {
|
|
40
|
+
boolean: (names: string[], fallback: boolean) => boolean;
|
|
41
|
+
list: (names: string[]) => string[];
|
|
42
|
+
optional: (names: string[]) => string | undefined;
|
|
43
|
+
positiveInteger: (names: string[], fallback: number) => number;
|
|
44
|
+
required: (names: string[]) => string;
|
|
45
|
+
};
|
|
46
|
+
/** Typed readers over an environment map; the first non-empty name wins. */
|
|
47
|
+
export declare const createEnvReader: (env: Env) => EnvReader;
|
|
48
|
+
/**
|
|
49
|
+
* Canonical server URL: scheme and host, optional base path, no trailing slash.
|
|
50
|
+
* A pasted endpoint URL (`…/api/qcli/mcp`) is accepted and reduced to the base.
|
|
51
|
+
*/
|
|
52
|
+
export declare const normalizeUrl: (input: string) => string;
|
|
53
|
+
export declare const loadConfig: (env?: Env) => Config;
|
|
54
|
+
/** One-line summary for the startup log (never contains secrets). */
|
|
55
|
+
export declare const describeConfig: (config: Config) => string;
|
|
56
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAErD,qBAAa,WAAY,SAAQ,KAAK;gBACtB,OAAO,EAAE,MAAM;CAI9B;AAED,6DAA6D;AAC7D,MAAM,MAAM,UAAU,GAChB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D,MAAM,MAAM,MAAM,GAAG;IACjB,IAAI,EAAE,UAAU,CAAC;IACjB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,0EAA0E;IAC1E,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,iEAAiE;IACjE,QAAQ,EAAE,OAAO,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,wEAAwE;IACxE,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,0CAA0C;IAC1C,SAAS,EAAE,OAAO,CAAC;IACnB,0EAA0E;IAC1E,GAAG,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,eAAO,MAAM,QAAQ,UAA0B,CAAC;AAEhD,6DAA6D;AAC7D,eAAO,MAAM,KAAK,GAAI,QAAQ,MAAM,KAAG,MAAM,EAAqD,CAAC;AAEnG,eAAO,MAAM,kBAAkB,UAAoB,CAAC;AAKpD,MAAM,MAAM,SAAS,GAAG;IACpB,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC;IACzD,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,MAAM,EAAE,CAAC;IACpC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,MAAM,GAAG,SAAS,CAAC;IAClD,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/D,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,MAAM,CAAC;CACzC,CAAC;AAEF,4EAA4E;AAC5E,eAAO,MAAM,eAAe,GAAI,KAAK,GAAG,KAAG,SAoD1C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,KAAG,MAY5C,CAAC;AAqBF,eAAO,MAAM,UAAU,GAAI,MAAK,GAAiB,KAAG,MAYnD,CAAC;AAEF,qEAAqE;AACrE,eAAO,MAAM,cAAc,GAAI,QAAQ,MAAM,KAAG,MAe/C,CAAC"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export class ConfigError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'ConfigError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export const PREFIXES = ['QCLI', 'QODALIS_CLI'];
|
|
8
|
+
/** `QCLI_<suffix>` plus the `QODALIS_CLI_<suffix>` alias. */
|
|
9
|
+
export const names = (suffix) => PREFIXES.map((prefix) => `${prefix}_${suffix}`);
|
|
10
|
+
export const ENV_FILE_VARIABLES = names('ENV_FILE');
|
|
11
|
+
const TRUE_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
|
12
|
+
const FALSE_VALUES = new Set(['0', 'false', 'no', 'off']);
|
|
13
|
+
/** Typed readers over an environment map; the first non-empty name wins. */
|
|
14
|
+
export const createEnvReader = (env) => {
|
|
15
|
+
const optional = (names) => {
|
|
16
|
+
for (const name of names) {
|
|
17
|
+
const value = env[name]?.trim();
|
|
18
|
+
if (value) {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
};
|
|
24
|
+
const describe = (names) => names.length > 1
|
|
25
|
+
? `${names[0]} (or its alias ${names.slice(1).join(' / ')})`
|
|
26
|
+
: (names[0] ?? '');
|
|
27
|
+
const required = (names) => {
|
|
28
|
+
const value = optional(names);
|
|
29
|
+
if (!value) {
|
|
30
|
+
throw new ConfigError(`${describe(names)} is required.`);
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
};
|
|
34
|
+
const boolean = (names, fallback) => {
|
|
35
|
+
const value = optional(names);
|
|
36
|
+
if (value === undefined) {
|
|
37
|
+
return fallback;
|
|
38
|
+
}
|
|
39
|
+
const normalized = value.toLowerCase();
|
|
40
|
+
if (TRUE_VALUES.has(normalized)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (FALSE_VALUES.has(normalized)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
throw new ConfigError(`${names[0]} must be a boolean (true/false), got "${value}".`);
|
|
47
|
+
};
|
|
48
|
+
const positiveInteger = (names, fallback) => {
|
|
49
|
+
const value = optional(names);
|
|
50
|
+
if (value === undefined) {
|
|
51
|
+
return fallback;
|
|
52
|
+
}
|
|
53
|
+
const parsed = Number(value);
|
|
54
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
55
|
+
throw new ConfigError(`${names[0]} must be a positive integer, got "${value}".`);
|
|
56
|
+
}
|
|
57
|
+
return parsed;
|
|
58
|
+
};
|
|
59
|
+
const list = (names) => (optional(names) ?? '')
|
|
60
|
+
.split(',')
|
|
61
|
+
.map((entry) => entry.trim())
|
|
62
|
+
.filter((entry) => entry.length > 0);
|
|
63
|
+
return { boolean, list, optional, positiveInteger, required };
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Canonical server URL: scheme and host, optional base path, no trailing slash.
|
|
67
|
+
* A pasted endpoint URL (`…/api/qcli/mcp`) is accepted and reduced to the base.
|
|
68
|
+
*/
|
|
69
|
+
export const normalizeUrl = (input) => {
|
|
70
|
+
let url;
|
|
71
|
+
try {
|
|
72
|
+
url = new URL(input);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw new ConfigError(`QCLI_URL must be an http(s) URL, got "${input}".`);
|
|
76
|
+
}
|
|
77
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
78
|
+
throw new ConfigError(`QCLI_URL must be an http(s) URL, got "${input}".`);
|
|
79
|
+
}
|
|
80
|
+
const path = url.pathname.replace(/\/+$/, '').replace(/\/api\/qcli\/mcp$/i, '');
|
|
81
|
+
return `${url.protocol}//${url.host}${path}`;
|
|
82
|
+
};
|
|
83
|
+
const loadAuth = (read) => {
|
|
84
|
+
const token = read.optional(names('TOKEN'));
|
|
85
|
+
const user = read.optional(names('USER'));
|
|
86
|
+
const password = read.optional(names('PASSWORD'));
|
|
87
|
+
if (token && (user || password)) {
|
|
88
|
+
throw new ConfigError('Set either QCLI_TOKEN or QCLI_USER + QCLI_PASSWORD, not both.');
|
|
89
|
+
}
|
|
90
|
+
if (token) {
|
|
91
|
+
return { kind: 'token', token };
|
|
92
|
+
}
|
|
93
|
+
if (user || password) {
|
|
94
|
+
if (!user || !password) {
|
|
95
|
+
throw new ConfigError('QCLI_USER and QCLI_PASSWORD must be set together.');
|
|
96
|
+
}
|
|
97
|
+
return { kind: 'password', password, user };
|
|
98
|
+
}
|
|
99
|
+
return { kind: 'none' };
|
|
100
|
+
};
|
|
101
|
+
export const loadConfig = (env = process.env) => {
|
|
102
|
+
const read = createEnvReader(env);
|
|
103
|
+
return {
|
|
104
|
+
auth: loadAuth(read),
|
|
105
|
+
connectionTimeoutMs: read.positiveInteger(names('CONNECTION_TIMEOUT'), 8000),
|
|
106
|
+
exclude: read.list(names('EXCLUDE')),
|
|
107
|
+
readOnly: read.boolean(names('READ_ONLY'), false),
|
|
108
|
+
requestTimeoutMs: read.positiveInteger(names('REQUEST_TIMEOUT'), 60000),
|
|
109
|
+
tools: read.list(names('TOOLS')),
|
|
110
|
+
trustCert: read.boolean(names('TRUST_CERT'), false),
|
|
111
|
+
url: normalizeUrl(read.required(names('URL'))),
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
/** One-line summary for the startup log (never contains secrets). */
|
|
115
|
+
export const describeConfig = (config) => {
|
|
116
|
+
const auth = config.auth.kind === 'token'
|
|
117
|
+
? 'token auth'
|
|
118
|
+
: config.auth.kind === 'password'
|
|
119
|
+
? `signs in as ${config.auth.user}`
|
|
120
|
+
: 'no auth';
|
|
121
|
+
const filters = [
|
|
122
|
+
config.readOnly ? 'read-only tools' : 'all tools',
|
|
123
|
+
config.tools.length ? `allow ${config.tools.join(',')}` : '',
|
|
124
|
+
config.exclude.length ? `exclude ${config.exclude.join(',')}` : '',
|
|
125
|
+
]
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.join(', ');
|
|
128
|
+
return `${config.url} (${auth}; ${filters})`;
|
|
129
|
+
};
|
|
130
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small `.env` parser (no dependency): `KEY=value`, optional `export`,
|
|
3
|
+
* `#` comments, single/double quotes with `\n`/`\t`/`\\`/`\"` escapes inside
|
|
4
|
+
* double quotes, and inline comments after unquoted values.
|
|
5
|
+
*/
|
|
6
|
+
export declare const parseEnvFile: (content: string) => Record<string, string>;
|
|
7
|
+
/** Loads a .env file into `env` without overriding keys that are already set. */
|
|
8
|
+
export declare const applyEnvFile: (path: string, env: Record<string, string | undefined>) => void;
|
|
9
|
+
//# sourceMappingURL=env-file.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-file.d.ts","sourceRoot":"","sources":["../../src/env-file.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,SAAS,MAAM,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAgBnE,CAAC;AAmBF,iFAAiF;AACjF,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,EAAE,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,KAAG,IAapF,CAAC"}
|