ap-test-mcp-cli 0.0.1
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 +120 -0
- package/bin/mcp-cli.js +7 -0
- package/package.json +18 -0
- package/scripts/load-env.sh +20 -0
- package/scripts/run-cli.sh +34 -0
- package/scripts/test-amap.sh +26 -0
- package/scripts/test.sh +12 -0
- package/src/cli.js +37 -0
- package/src/commands/auth.js +40 -0
- package/src/commands/prompt.js +50 -0
- package/src/commands/resource.js +43 -0
- package/src/commands/serve.js +25 -0
- package/src/commands/server.js +62 -0
- package/src/commands/tool.js +105 -0
- package/src/shared/amap.js +22 -0
- package/src/shared/mcp.js +57 -0
- package/src/shared/output.js +91 -0
- package/src/shared/request.js +35 -0
- package/src/shared/server.js +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# mcp-cli
|
|
2
|
+
|
|
3
|
+
MCP(Model Context Protocol)命令行客户端:管理 MCP 服务、列出/调用 tools、资源与 prompts。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 安装与运行
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**运行方式(任选其一):**
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm start -- <子命令> [选项]
|
|
17
|
+
# 或
|
|
18
|
+
node bin/mcp-cli.js -- <子命令> [选项]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
沙箱/CI 等无法 `npm install -g` 时,用上述方式在项目目录下执行即可。
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 全局选项
|
|
26
|
+
|
|
27
|
+
| 选项 | 说明 |
|
|
28
|
+
|------|------|
|
|
29
|
+
| `-s, --server <name>` | 指定 MCP 服务器名,部分子命令必填 |
|
|
30
|
+
| `--json` | 输出机器可读 JSON |
|
|
31
|
+
| `-v, --version` | 显示版本 |
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 命令一览
|
|
36
|
+
|
|
37
|
+
### server — 服务器列表与连通性
|
|
38
|
+
|
|
39
|
+
| 子命令 | 说明 | 选项 | 状态 |
|
|
40
|
+
|--------|------|------|------|
|
|
41
|
+
| `server list` | 列出已配置的 MCP 服务器 | — | ✅ 高德(见 MCP_AMAP_*) |
|
|
42
|
+
| `server check` | 检查指定/全部服务器连通性 | `-s`;`--all` | ✅ 高德 |
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
### tool — 工具列表、详情与调用
|
|
47
|
+
|
|
48
|
+
| 子命令 | 说明 | 必填/选项 | 状态 |
|
|
49
|
+
|--------|------|-----------|------|
|
|
50
|
+
| `tool list` | 列出可用工具 | `-s`;`--save <file>`、`--allow-tools <csv>` | ✅ 高德 |
|
|
51
|
+
| `tool get` | 查看单个工具详情 | `-s`、`--tool <name>`;`--allow-tools <csv>` | ✅ 高德 |
|
|
52
|
+
| `tool call` | 执行工具 | `-s`、`--tool <name>`、`--input <json>` | ✅ 高德 |
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
### resource — 资源列表、详情与读取
|
|
57
|
+
|
|
58
|
+
| 子命令 | 说明 | 必填/选项 | 状态 |
|
|
59
|
+
|--------|------|-----------|------|
|
|
60
|
+
| `resource list` | 列出资源 | `-s` | ⏳ 占位 |
|
|
61
|
+
| `resource get` | 资源元信息 | `-s`、`--resource <path_or_id>` | ⏳ 占位 |
|
|
62
|
+
| `resource read` | 读取资源内容 | `-s`、`--resource <path_or_id>`;`--format text\|json` | ⏳ 占位 |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### prompt — 提示词列表、详情与调用
|
|
67
|
+
|
|
68
|
+
| 子命令 | 说明 | 必填/选项 | 状态 |
|
|
69
|
+
|--------|------|-----------|------|
|
|
70
|
+
| `prompt list` | 列出 prompts | `-s` | ⏳ 占位 |
|
|
71
|
+
| `prompt get` | 单个 prompt 详情 | `-s`、`--prompt <name>` | ⏳ 占位 |
|
|
72
|
+
| `prompt call` | 调用 prompt | `-s`、`--prompt <name>`;`--args <json>` | ⏳ 占位 |
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
### auth — OAuth 登录 / 登出 / 状态
|
|
77
|
+
|
|
78
|
+
| 子命令 | 说明 | 选项 | 状态 |
|
|
79
|
+
|--------|------|------|------|
|
|
80
|
+
| `auth login` | 登录 OAuth 服务 | `-s`;`--force` | ⏳ 占位 |
|
|
81
|
+
| `auth logout` | 清除本地令牌 | `-s` | ⏳ 占位 |
|
|
82
|
+
| `auth status` | 查看登录状态 | `-s` | ⏳ 占位 |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### serve — 启动 MCP RPC 服务端
|
|
87
|
+
|
|
88
|
+
| 说明 | 选项 | 状态 |
|
|
89
|
+
|------|------|------|
|
|
90
|
+
| 本地启动 MCP 服务(供客户端连接) | `--port <port>`(默认 8080)、`--config <path>` | ⏳ 占位 |
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 环境变量
|
|
95
|
+
|
|
96
|
+
脚本会按顺序加载:`.env` → `.env.local` → `.env.local.test`,后加载覆盖前者。
|
|
97
|
+
|
|
98
|
+
**高德 MCP(`-s amap`):**
|
|
99
|
+
|
|
100
|
+
- `MCP_AMAP_KEY` — 高德开放平台 Key(会拼成 `https://mcp.amap.com/mcp?key=...`)
|
|
101
|
+
- 或 `MCP_AMAP_URL` — 完整 MCP 端点 URL(含 key 等)
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 高德 MCP 测试
|
|
106
|
+
|
|
107
|
+
配置上述任一变量后:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pnpm run test:amap
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
会依次执行:`server list`、`server check -s amap`、`tool list -s amap`、`tool call -s amap --tool maps_weather --input '{"city":"北京"}'`。未配置则跳过并退出 0。
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 当前实现状态
|
|
118
|
+
|
|
119
|
+
- **已实现:** `server list/check`、`tool list/get/call` 支持高德 MCP(`-s amap`),底层为 `shared/request.js` + `shared/mcp.js` + `shared/amap.js`。
|
|
120
|
+
- **占位:** `resource`、`prompt`、`auth`、`serve` 仅有子命令骨架。
|
package/bin/mcp-cli.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ap-test-mcp-cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "MCP command line tool",
|
|
5
|
+
"bin": {
|
|
6
|
+
"mcp-cli": "bin/mcp-cli.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "commonjs",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "bash ./scripts/run-cli.sh",
|
|
11
|
+
"test": "bash ./scripts/test.sh",
|
|
12
|
+
"test:amap": "bash ./scripts/test-amap.sh"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"commander": "^12.1.0"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT"
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
|
+
|
|
6
|
+
load_file() {
|
|
7
|
+
local file="$1"
|
|
8
|
+
if [[ -f "$file" ]]; then
|
|
9
|
+
set -a
|
|
10
|
+
# shellcheck disable=SC1090
|
|
11
|
+
source "$file"
|
|
12
|
+
set +a
|
|
13
|
+
fi
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
load_env() {
|
|
17
|
+
load_file "$ROOT_DIR/.env"
|
|
18
|
+
load_file "$ROOT_DIR/.env.local"
|
|
19
|
+
load_file "$ROOT_DIR/.env.local.test"
|
|
20
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
|
+
# shellcheck disable=SC1090
|
|
6
|
+
source "$ROOT_DIR/scripts/load-env.sh"
|
|
7
|
+
load_env
|
|
8
|
+
|
|
9
|
+
ARGS=("$@")
|
|
10
|
+
|
|
11
|
+
if [[ "${#ARGS[@]}" -ge 3 ]]; then
|
|
12
|
+
COMMAND="${ARGS[0]}"
|
|
13
|
+
LAST_INDEX=$(( ${#ARGS[@]} - 1 ))
|
|
14
|
+
LAST_VALUE="${ARGS[$LAST_INDEX]}"
|
|
15
|
+
|
|
16
|
+
HAS_SERVER_FLAG=0
|
|
17
|
+
for token in "${ARGS[@]}"; do
|
|
18
|
+
if [[ "$token" == "-s" || "$token" == "--server" || "$token" == --server=* ]]; then
|
|
19
|
+
HAS_SERVER_FLAG=1
|
|
20
|
+
break
|
|
21
|
+
fi
|
|
22
|
+
done
|
|
23
|
+
|
|
24
|
+
if [[ $HAS_SERVER_FLAG -eq 0 && "$LAST_VALUE" != -* ]]; then
|
|
25
|
+
case "$COMMAND" in
|
|
26
|
+
server|tool|auth|prompt|resource)
|
|
27
|
+
unset 'ARGS[$LAST_INDEX]'
|
|
28
|
+
ARGS+=("-s" "$LAST_VALUE")
|
|
29
|
+
;;
|
|
30
|
+
esac
|
|
31
|
+
fi
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
exec node "$ROOT_DIR/bin/mcp-cli.js" "${ARGS[@]}"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
|
+
# shellcheck disable=SC1090
|
|
6
|
+
source "$ROOT_DIR/scripts/load-env.sh"
|
|
7
|
+
load_env
|
|
8
|
+
|
|
9
|
+
if [[ -z "${MCP_AMAP_KEY:-}" && -z "${MCP_AMAP_URL:-}" ]]; then
|
|
10
|
+
echo "Skip Amap test: set MCP_AMAP_KEY or MCP_AMAP_URL in .env.local or .env.local.test"
|
|
11
|
+
exit 0
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
echo "[1/4] server list"
|
|
15
|
+
node "$ROOT_DIR/bin/mcp-cli.js" server list
|
|
16
|
+
|
|
17
|
+
echo "[2/4] server check -s amap"
|
|
18
|
+
node "$ROOT_DIR/bin/mcp-cli.js" server check -s amap
|
|
19
|
+
|
|
20
|
+
echo "[3/4] tool list -s amap"
|
|
21
|
+
node "$ROOT_DIR/bin/mcp-cli.js" tool list -s amap
|
|
22
|
+
|
|
23
|
+
echo "[4/4] tool call -s amap maps_weather 北京"
|
|
24
|
+
node "$ROOT_DIR/bin/mcp-cli.js" tool call -s amap --tool maps_weather --input '{"city":"北京"}'
|
|
25
|
+
|
|
26
|
+
echo "Amap test passed"
|
package/scripts/test.sh
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
|
+
|
|
6
|
+
echo "[1/2] server list"
|
|
7
|
+
node "$ROOT_DIR/bin/mcp-cli.js" server list
|
|
8
|
+
|
|
9
|
+
echo "[2/2] tool list (requires -s; no backend)"
|
|
10
|
+
node "$ROOT_DIR/bin/mcp-cli.js" tool list -s example
|
|
11
|
+
|
|
12
|
+
echo "Test passed"
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { Command } = require("commander");
|
|
4
|
+
const { registerServerCommand } = require("./commands/server");
|
|
5
|
+
const { registerAuthCommand } = require("./commands/auth");
|
|
6
|
+
const { registerToolCommand } = require("./commands/tool");
|
|
7
|
+
const { registerPromptCommand } = require("./commands/prompt");
|
|
8
|
+
const { registerResourceCommand } = require("./commands/resource");
|
|
9
|
+
const { registerServeCommand } = require("./commands/serve");
|
|
10
|
+
|
|
11
|
+
function run(argv) {
|
|
12
|
+
const program = new Command();
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name("mcp-cli")
|
|
16
|
+
.description(
|
|
17
|
+
"A comprehensive command-line interface for interacting with Model Context Protocol servers.\n" +
|
|
18
|
+
"This CLI provides a unified way to manage MCP servers, execute tools, read resources,\n" +
|
|
19
|
+
"call prompts, and handle OAuth authentication across multiple MCP server instances."
|
|
20
|
+
)
|
|
21
|
+
.option("-s, --server <string>", "specify the server name")
|
|
22
|
+
.option("--json", "output machine-readable JSON")
|
|
23
|
+
.version("0.1.0", "-v, --version", "version for mcp-cli")
|
|
24
|
+
.showHelpAfterError();
|
|
25
|
+
program.configureHelp({ showGlobalOptions: true });
|
|
26
|
+
|
|
27
|
+
registerServerCommand(program);
|
|
28
|
+
registerAuthCommand(program);
|
|
29
|
+
registerToolCommand(program);
|
|
30
|
+
registerPromptCommand(program);
|
|
31
|
+
registerResourceCommand(program);
|
|
32
|
+
registerServeCommand(program);
|
|
33
|
+
|
|
34
|
+
program.parse(argv);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { run };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { requireGlobalServer } = require("../shared/server");
|
|
4
|
+
|
|
5
|
+
function registerAuthCommand(program) {
|
|
6
|
+
const auth = program
|
|
7
|
+
.command("auth")
|
|
8
|
+
.description(
|
|
9
|
+
"Handle OAuth 2.1 authentication for MCP servers including login, logout, and status checking."
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
auth
|
|
13
|
+
.command("login")
|
|
14
|
+
.description("Authenticate with an OAuth-enabled MCP server")
|
|
15
|
+
.option("--force", "force re-login")
|
|
16
|
+
.action((options, command) => {
|
|
17
|
+
const server = requireGlobalServer(command, "auth login");
|
|
18
|
+
console.log(`Auth login for "${server}" (force=${Boolean(options.force)})... [stub]`);
|
|
19
|
+
console.log("Visit https://example.com/oauth/authorize to authorize");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
auth
|
|
23
|
+
.command("logout")
|
|
24
|
+
.description("Clear stored OAuth tokens")
|
|
25
|
+
.action((_, command) => {
|
|
26
|
+
const server = requireGlobalServer(command, "auth logout");
|
|
27
|
+
console.log(`Auth logout for "${server}"... [stub]`);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
auth
|
|
31
|
+
.command("status")
|
|
32
|
+
.description("Show authentication status")
|
|
33
|
+
.action((_, command) => {
|
|
34
|
+
const server = requireGlobalServer(command, "auth status");
|
|
35
|
+
console.log(`Auth status for "${server}"... [stub]`);
|
|
36
|
+
console.log("Authenticated, expires in 2h");
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { registerAuthCommand };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { requireGlobalServer } = require("../shared/server");
|
|
4
|
+
|
|
5
|
+
function parseJsonFlag(raw, flagName) {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(raw);
|
|
8
|
+
} catch (err) {
|
|
9
|
+
console.error(`Invalid JSON for --${flagName}: ${err.message}`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function registerPromptCommand(program) {
|
|
15
|
+
const prompt = program
|
|
16
|
+
.command("prompt")
|
|
17
|
+
.description("List, inspect, and call prompts on MCP servers with optional arguments.");
|
|
18
|
+
|
|
19
|
+
prompt
|
|
20
|
+
.command("list")
|
|
21
|
+
.description("List available prompts")
|
|
22
|
+
.action((_, command) => {
|
|
23
|
+
const server = requireGlobalServer(command, "prompt list");
|
|
24
|
+
console.log(`Prompts for "${server}"... [stub]`);
|
|
25
|
+
console.log("summarize-thread, draft-reply");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
prompt
|
|
29
|
+
.command("get")
|
|
30
|
+
.description("Get detailed information about a specific prompt")
|
|
31
|
+
.requiredOption("--prompt <prompt_name>", "prompt name")
|
|
32
|
+
.action((options, command) => {
|
|
33
|
+
const server = requireGlobalServer(command, "prompt get");
|
|
34
|
+
console.log(`Prompt details for "${options.prompt}" on "${server}"... [stub]`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
prompt
|
|
38
|
+
.command("call")
|
|
39
|
+
.description("Call a prompt on an MCP server")
|
|
40
|
+
.requiredOption("--prompt <prompt_name>", "prompt name")
|
|
41
|
+
.option("--args <json>", "prompt args JSON")
|
|
42
|
+
.action((options, command) => {
|
|
43
|
+
const server = requireGlobalServer(command, "prompt call");
|
|
44
|
+
const parsedArgs = options.args ? parseJsonFlag(options.args, "args") : {};
|
|
45
|
+
console.log(`Calling prompt "${options.prompt}" on "${server}"... [stub]`);
|
|
46
|
+
console.log(JSON.stringify(parsedArgs, null, 2));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { registerPromptCommand };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { requireGlobalServer } = require("../shared/server");
|
|
4
|
+
|
|
5
|
+
function registerResourceCommand(program) {
|
|
6
|
+
const resource = program
|
|
7
|
+
.command("resource")
|
|
8
|
+
.description("List, read, and inspect resources available on MCP servers.");
|
|
9
|
+
|
|
10
|
+
resource
|
|
11
|
+
.command("list")
|
|
12
|
+
.description("List available resources")
|
|
13
|
+
.action((_, command) => {
|
|
14
|
+
const server = requireGlobalServer(command, "resource list");
|
|
15
|
+
console.log(`Resources for "${server}"... [stub]`);
|
|
16
|
+
console.log("file:///notes/report.md");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
resource
|
|
20
|
+
.command("get")
|
|
21
|
+
.description("Get detailed information about a specific resource")
|
|
22
|
+
.requiredOption("--resource <path_or_id>", "resource path or id")
|
|
23
|
+
.action((options, command) => {
|
|
24
|
+
const server = requireGlobalServer(command, "resource get");
|
|
25
|
+
console.log(`Resource metadata for "${options.resource}" on "${server}"... [stub]`);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
resource
|
|
29
|
+
.command("read")
|
|
30
|
+
.description("Read a resource from an MCP server")
|
|
31
|
+
.requiredOption("--resource <path_or_id>", "resource path or id")
|
|
32
|
+
.option("--format <format>", "output format: text|json", "text")
|
|
33
|
+
.action((options, command) => {
|
|
34
|
+
const server = requireGlobalServer(command, "resource read");
|
|
35
|
+
if (!["text", "json"].includes(options.format)) {
|
|
36
|
+
console.error('[resource read] --format must be "text" or "json"');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
console.log(`Reading resource "${options.resource}" on "${server}" as ${options.format}... [stub]`);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { registerResourceCommand };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { InvalidArgumentError } = require("commander");
|
|
4
|
+
|
|
5
|
+
function parsePort(portRaw) {
|
|
6
|
+
const port = Number(portRaw);
|
|
7
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
8
|
+
throw new InvalidArgumentError("port must be a positive integer");
|
|
9
|
+
}
|
|
10
|
+
return port;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function registerServeCommand(program) {
|
|
14
|
+
program
|
|
15
|
+
.command("serve")
|
|
16
|
+
.description("Start the MCP RPC server")
|
|
17
|
+
.option("--port <port>", "listen port", parsePort, 8080)
|
|
18
|
+
.option("--config <path>", "config file path")
|
|
19
|
+
.action((options) => {
|
|
20
|
+
const config = options.config ? ` with config "${options.config}"` : "";
|
|
21
|
+
console.log(`Serving MCP at localhost:${options.port}${config} [stub]`);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = { registerServeCommand };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { CliError, ok, wrapAction } = require("../shared/output");
|
|
4
|
+
const { postMcp } = require("../shared/mcp");
|
|
5
|
+
const { AMAP_SERVER, getAmapTransport, getAmapUrl } = require("../shared/amap");
|
|
6
|
+
|
|
7
|
+
function registerServerCommand(program) {
|
|
8
|
+
const server = program
|
|
9
|
+
.command("server")
|
|
10
|
+
.description("List enabled MCP servers and check their connectivity and capabilities.");
|
|
11
|
+
|
|
12
|
+
server
|
|
13
|
+
.command("list")
|
|
14
|
+
.description("List available MCP servers")
|
|
15
|
+
.action(wrapAction("server.list", async (_, command) => {
|
|
16
|
+
const servers = [];
|
|
17
|
+
if (getAmapUrl()) {
|
|
18
|
+
servers.push({ name: AMAP_SERVER, status: "configured" });
|
|
19
|
+
}
|
|
20
|
+
const message = servers.length ? `Available: ${servers.map((s) => s.name).join(", ")}` : "No servers configured";
|
|
21
|
+
ok(command, "server.list", message, { servers });
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
server
|
|
25
|
+
.command("check")
|
|
26
|
+
.description("Check connectivity to MCP servers")
|
|
27
|
+
.option("--all", "check all servers")
|
|
28
|
+
.action(wrapAction("server.check", async (options, command) => {
|
|
29
|
+
const serverName = command.optsWithGlobals().server;
|
|
30
|
+
if (options.all || !serverName) {
|
|
31
|
+
const results = [];
|
|
32
|
+
if (getAmapUrl()) {
|
|
33
|
+
const transport = getAmapTransport();
|
|
34
|
+
try {
|
|
35
|
+
await postMcp(transport, "initialize", { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "mcp-cli", version: "0.1.0" } });
|
|
36
|
+
const listRes = await postMcp(transport, "tools/list", {});
|
|
37
|
+
const count = listRes?.result?.tools?.length ?? 0;
|
|
38
|
+
results.push({ server: AMAP_SERVER, connected: true, tools: count });
|
|
39
|
+
} catch (err) {
|
|
40
|
+
results.push({ server: AMAP_SERVER, connected: false, error: err.message });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const message = results.length ? results.map((r) => `${r.server}: ${r.connected ? `connected, ${r.tools} tools` : r.error}`).join("; ") : "No servers configured";
|
|
44
|
+
ok(command, "server.check", message, { servers: results });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (serverName === AMAP_SERVER) {
|
|
48
|
+
const transport = getAmapTransport();
|
|
49
|
+
if (!transport) {
|
|
50
|
+
throw new CliError("[server check] Amap not configured. Set MCP_AMAP_KEY or MCP_AMAP_URL.", "ERR_AMAP_NOT_CONFIGURED");
|
|
51
|
+
}
|
|
52
|
+
await postMcp(transport, "initialize", { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "mcp-cli", version: "0.1.0" } });
|
|
53
|
+
const listRes = await postMcp(transport, "tools/list", {});
|
|
54
|
+
const count = listRes?.result?.tools?.length ?? 0;
|
|
55
|
+
ok(command, "server.check", `Server "${AMAP_SERVER}": connected, ${count} tools`, { server: AMAP_SERVER, connected: true, tools: count });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
ok(command, "server.check", `Server "${serverName}": no backend configured`, { server: serverName, connected: false });
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { registerServerCommand };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const { CliError, ok, wrapAction } = require("../shared/output");
|
|
5
|
+
const { postMcp } = require("../shared/mcp");
|
|
6
|
+
const { AMAP_SERVER, getAmapTransport } = require("../shared/amap");
|
|
7
|
+
|
|
8
|
+
function getRequiredServer(command, context) {
|
|
9
|
+
const server = command.optsWithGlobals().server;
|
|
10
|
+
if (!server) {
|
|
11
|
+
throw new CliError(`[${context}] Missing required global flag: -s or --server`, "ERR_MISSING_SERVER");
|
|
12
|
+
}
|
|
13
|
+
return server;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseJsonFlag(raw, flagName) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(raw);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
throw new CliError(`Invalid JSON for --${flagName}: ${err.message}`, "ERR_INVALID_JSON");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function amapToolsList(transport, options) {
|
|
25
|
+
await postMcp(transport, "initialize", { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "mcp-cli", version: "0.1.0" } });
|
|
26
|
+
const response = await postMcp(transport, "tools/list", {});
|
|
27
|
+
const tools = response?.result?.tools ?? [];
|
|
28
|
+
if (options?.save) {
|
|
29
|
+
fs.writeFileSync(options.save, JSON.stringify(response, null, 2), "utf8");
|
|
30
|
+
}
|
|
31
|
+
return { tools, response };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function registerToolCommand(program) {
|
|
35
|
+
const tool = program
|
|
36
|
+
.command("tool")
|
|
37
|
+
.description("List, inspect, and execute tools on MCP servers with JSON input support.");
|
|
38
|
+
|
|
39
|
+
tool
|
|
40
|
+
.command("list")
|
|
41
|
+
.description("List available tools")
|
|
42
|
+
.option("--save <file>", "save output path")
|
|
43
|
+
.option("--allow-tools <csv>", "comma-separated allowed tools")
|
|
44
|
+
.action(wrapAction("tool.list", async (options, command) => {
|
|
45
|
+
const server = getRequiredServer(command, "tool list");
|
|
46
|
+
if (server === AMAP_SERVER) {
|
|
47
|
+
const transport = getAmapTransport();
|
|
48
|
+
if (!transport) throw new CliError("[tool list] Amap not configured. Set MCP_AMAP_KEY or MCP_AMAP_URL.", "ERR_AMAP_NOT_CONFIGURED");
|
|
49
|
+
const { tools, response } = await amapToolsList(transport, options);
|
|
50
|
+
ok(command, "tool.list", `Tools for server "${server}"`, {
|
|
51
|
+
server,
|
|
52
|
+
count: tools.length,
|
|
53
|
+
tools,
|
|
54
|
+
savedTo: options.save || null
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
ok(command, "tool.list", `Tools for server "${server}" (no backend configured)`, {
|
|
59
|
+
server,
|
|
60
|
+
count: 0,
|
|
61
|
+
tools: [],
|
|
62
|
+
savedTo: options.save || null
|
|
63
|
+
});
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
tool
|
|
67
|
+
.command("get")
|
|
68
|
+
.description("Get detailed information about a specific tool")
|
|
69
|
+
.requiredOption("--tool <tool_name>", "tool name")
|
|
70
|
+
.option("--allow-tools <csv>", "comma-separated allowed tools")
|
|
71
|
+
.action(wrapAction("tool.get", async (options, command) => {
|
|
72
|
+
const server = getRequiredServer(command, "tool get");
|
|
73
|
+
if (server === AMAP_SERVER) {
|
|
74
|
+
const transport = getAmapTransport();
|
|
75
|
+
if (!transport) throw new CliError("[tool get] Amap not configured. Set MCP_AMAP_KEY or MCP_AMAP_URL.", "ERR_AMAP_NOT_CONFIGURED");
|
|
76
|
+
const { tools } = await amapToolsList(transport, {});
|
|
77
|
+
const found = tools.find((t) => t.name === options.tool);
|
|
78
|
+
if (!found) throw new CliError(`Tool "${options.tool}" not found.`, "ERR_TOOL_NOT_FOUND");
|
|
79
|
+
ok(command, "tool.get", `Tool "${options.tool}"`, found);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
throw new CliError(`[tool get] No backend configured for server "${server}".`, "ERR_NO_BACKEND");
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
tool
|
|
86
|
+
.command("call")
|
|
87
|
+
.description("Execute a tool on an MCP server")
|
|
88
|
+
.requiredOption("--tool <tool_name>", "tool name")
|
|
89
|
+
.requiredOption("--input <json>", "tool input JSON")
|
|
90
|
+
.action(wrapAction("tool.call", async (options, command) => {
|
|
91
|
+
const server = getRequiredServer(command, "tool call");
|
|
92
|
+
if (server === AMAP_SERVER) {
|
|
93
|
+
const transport = getAmapTransport();
|
|
94
|
+
if (!transport) throw new CliError("[tool call] Amap not configured. Set MCP_AMAP_KEY or MCP_AMAP_URL.", "ERR_AMAP_NOT_CONFIGURED");
|
|
95
|
+
const payload = parseJsonFlag(options.input, "input");
|
|
96
|
+
await postMcp(transport, "initialize", { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "mcp-cli", version: "0.1.0" } });
|
|
97
|
+
const response = await postMcp(transport, "tools/call", { name: options.tool, arguments: payload });
|
|
98
|
+
ok(command, "tool.call", `Called tool "${options.tool}"`, response?.result ?? response);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
throw new CliError(`[tool call] No backend configured for server "${server}".`, "ERR_NO_BACKEND");
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { registerToolCommand };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { createTransport } = require("./mcp");
|
|
4
|
+
|
|
5
|
+
const AMAP_SERVER = "amap";
|
|
6
|
+
const AMAP_MCP_BASE = "https://mcp.amap.com/mcp";
|
|
7
|
+
|
|
8
|
+
function getAmapUrl() {
|
|
9
|
+
const url = process.env.MCP_AMAP_URL;
|
|
10
|
+
if (url) return url;
|
|
11
|
+
const key = process.env.MCP_AMAP_KEY;
|
|
12
|
+
if (key) return `${AMAP_MCP_BASE}?key=${key}`;
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getAmapTransport() {
|
|
17
|
+
const url = getAmapUrl();
|
|
18
|
+
if (!url) return null;
|
|
19
|
+
return createTransport(url);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { AMAP_SERVER, getAmapUrl, getAmapTransport };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { randomUUID } = require("node:crypto");
|
|
4
|
+
const { request } = require("./request");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 通用 MCP 协议层:用给定的 transport 发一条 JSON-RPC,解析并返回整包;出错抛 Error。
|
|
8
|
+
* 不绑定任何具体 server(飞书/高德等),只关心「拼 JSON-RPC、调 transport、解析结果」。
|
|
9
|
+
*
|
|
10
|
+
* @param {(body: object) => Promise<string>} transport - 接收 JSON-RPC 对象,返回响应文本
|
|
11
|
+
* @param {string} method - MCP 方法名,如 "initialize" | "tools/list" | "tools/call"
|
|
12
|
+
* @param {object | null} [params] - 方法参数
|
|
13
|
+
* @returns {Promise<object>} 整包 JSON-RPC 响应(含 result / error)
|
|
14
|
+
*/
|
|
15
|
+
async function postMcp(transport, method, params) {
|
|
16
|
+
const body = {
|
|
17
|
+
jsonrpc: "2.0",
|
|
18
|
+
id: randomUUID(),
|
|
19
|
+
method,
|
|
20
|
+
params: params === undefined ? null : params
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const text = await transport(body);
|
|
24
|
+
let parsed = null;
|
|
25
|
+
try {
|
|
26
|
+
parsed = text ? JSON.parse(text) : null;
|
|
27
|
+
} catch {
|
|
28
|
+
parsed = { raw: text };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (parsed && parsed.error) {
|
|
32
|
+
const err = parsed.error;
|
|
33
|
+
const message = err.message || JSON.stringify(err);
|
|
34
|
+
throw new Error(`MCP error (${method}): ${message}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 根据 URL 和固定 headers 生成一个 transport 函数,供 postMcp 使用。
|
|
42
|
+
* 适用于鉴权在 query 或 header 里、且每次请求 headers 固定的场景(如高德 key 在 query)。
|
|
43
|
+
*
|
|
44
|
+
* @param {string} url - 完整 MCP 端点 URL
|
|
45
|
+
* @param {Record<string, string>} [headers={}] - 每次请求都带上的 headers(如 Accept、MCP-Protocol-Version)
|
|
46
|
+
* @returns {(body: object) => Promise<string>}
|
|
47
|
+
*/
|
|
48
|
+
function createTransport(url, headers = {}) {
|
|
49
|
+
const baseHeaders = {
|
|
50
|
+
"Accept": "application/json, text/event-stream",
|
|
51
|
+
"MCP-Protocol-Version": "2024-11-05",
|
|
52
|
+
...headers
|
|
53
|
+
};
|
|
54
|
+
return (body) => request(url, { headers: baseHeaders, body: JSON.stringify(body) });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { postMcp, createTransport, request };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
class CliError extends Error {
|
|
4
|
+
constructor(message, code = "ERR_CLI", details = null) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "CliError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.details = details;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isJsonMode(command) {
|
|
13
|
+
try {
|
|
14
|
+
return Boolean(command && command.optsWithGlobals && command.optsWithGlobals().json);
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function writeLine(stream, text) {
|
|
21
|
+
stream.write(`${text}\n`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function emitResult(command, result) {
|
|
25
|
+
const jsonMode = isJsonMode(command);
|
|
26
|
+
const target = result.ok ? process.stdout : process.stderr;
|
|
27
|
+
|
|
28
|
+
if (jsonMode) {
|
|
29
|
+
writeLine(target, JSON.stringify(result, null, 2));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (result.ok) {
|
|
34
|
+
if (result.message) {
|
|
35
|
+
writeLine(process.stdout, result.message);
|
|
36
|
+
}
|
|
37
|
+
if (result.data !== undefined) {
|
|
38
|
+
writeLine(process.stdout, JSON.stringify(result.data, null, 2));
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
writeLine(process.stderr, `[${result.error.code}] ${result.error.message}`);
|
|
44
|
+
if (result.error.details) {
|
|
45
|
+
writeLine(process.stderr, JSON.stringify(result.error.details, null, 2));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ok(command, action, message, data) {
|
|
50
|
+
emitResult(command, {
|
|
51
|
+
ok: true,
|
|
52
|
+
action,
|
|
53
|
+
message,
|
|
54
|
+
data
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function fail(command, action, error) {
|
|
59
|
+
const normalized =
|
|
60
|
+
error instanceof CliError
|
|
61
|
+
? error
|
|
62
|
+
: new CliError(error && error.message ? error.message : String(error), "ERR_RUNTIME");
|
|
63
|
+
|
|
64
|
+
emitResult(command, {
|
|
65
|
+
ok: false,
|
|
66
|
+
action,
|
|
67
|
+
error: {
|
|
68
|
+
code: normalized.code || "ERR_RUNTIME",
|
|
69
|
+
message: normalized.message,
|
|
70
|
+
details: normalized.details || null
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function wrapAction(action, handler) {
|
|
77
|
+
return async (options, command) => {
|
|
78
|
+
try {
|
|
79
|
+
await handler(options, command);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
fail(command, action, error);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
CliError,
|
|
88
|
+
ok,
|
|
89
|
+
fail,
|
|
90
|
+
wrapAction
|
|
91
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 底层 HTTP:对指定 URL 发 POST,带 headers 和 body,返回响应文本。
|
|
5
|
+
* 不关心 MCP、不关心具体业务,只负责「发请求、拿响应」。
|
|
6
|
+
* @param {string} url - 完整 URL(可含 query,如 ?key=xxx)
|
|
7
|
+
* @param {{ headers?: Record<string, string>; body: string }} options
|
|
8
|
+
* @returns {Promise<string>} 响应体文本
|
|
9
|
+
*/
|
|
10
|
+
async function request(url, { headers = {}, body }) {
|
|
11
|
+
const response = await fetch(url, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: {
|
|
14
|
+
"Content-Type": "application/json",
|
|
15
|
+
...headers
|
|
16
|
+
},
|
|
17
|
+
body
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const text = await response.text();
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
let message = `HTTP ${response.status}`;
|
|
23
|
+
try {
|
|
24
|
+
const json = JSON.parse(text);
|
|
25
|
+
if (json.error && json.error.message) message = json.error.message;
|
|
26
|
+
else if (json.msg) message = json.msg;
|
|
27
|
+
} catch {
|
|
28
|
+
// keep default message
|
|
29
|
+
}
|
|
30
|
+
throw new Error(message);
|
|
31
|
+
}
|
|
32
|
+
return text;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { request };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function requireGlobalServer(command, context) {
|
|
4
|
+
const server = command.optsWithGlobals().server;
|
|
5
|
+
if (!server) {
|
|
6
|
+
console.error(`[${context}] Missing required global flag: -s or --server`);
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
return server;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
module.exports = { requireGlobalServer };
|