openclaw-mcp-router 1.0.1 โ 1.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/.github/pull_request_template.md +14 -0
- package/README.md +103 -72
- package/docs/CLI_FIRST_WORKFLOW.md +76 -0
- package/openclaw.plugin.json +162 -33
- package/package.json +1 -1
- package/skills/mcp-router/SKILL.md +15 -5
- package/skills/mcp-server-manager/SKILL.md +89 -0
- package/skills/packages/mcp-router.skill +0 -0
- package/skills/packages/mcp-server-manager.skill +0 -0
- package/src/config.ts +6 -1
- package/src/index.ts +15 -2
- package/src/indexer.ts +52 -0
- package/src/setup/setup-command.ts +28 -2
- package/src/test/config.test.ts +2 -0
- package/src/test/indexer.test.ts +1 -1
- package/src/test/tools/mcp-search-tool.test.ts +10 -1
- package/src/tools/mcp-search-tool.ts +56 -11
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
## Summary
|
|
2
|
+
-
|
|
3
|
+
|
|
4
|
+
## Why
|
|
5
|
+
-
|
|
6
|
+
|
|
7
|
+
## Validation
|
|
8
|
+
- [ ] `npm test` passes locally
|
|
9
|
+
|
|
10
|
+
## Documentation Checklist
|
|
11
|
+
- [ ] README updated (if behavior/config/user workflow changed)
|
|
12
|
+
- [ ] docs/ updated to match README and implementation
|
|
13
|
+
- [ ] No README/docs drift introduced
|
|
14
|
+
- [ ] skills/ content updated to match behavior/config changes (if applicable)
|
package/README.md
CHANGED
|
@@ -1,129 +1,160 @@
|
|
|
1
1
|
# OpenClaw MCP Router ๐
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
OpenClaw MCP Router is an OpenClaw plugin that keeps MCP tool catalogs out of the system prompt until needed.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Instead of injecting every MCP schema up front, it provides two lightweight meta-tools:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- `mcp_search` โ discover the right tool at runtime
|
|
8
|
+
- `mcp_call` โ execute as JSON fallback
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
* **Performance Hit:** Massive system prompts degrade reasoning accuracy (the "lost in the middle" phenomenon).
|
|
11
|
-
* **Cost:** High token usage leads to higher API costs for every turn of the conversation.
|
|
10
|
+
This cuts context bloat and improves tool selection quality on large MCP catalogs.
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Instead of a full schema dump, this plugin registers two lightweight "Meta-Tools":
|
|
12
|
+
---
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
2. **`mcp_call(tool_name, params)`**: Dynamically resolves the owning MCP server and executes the call.
|
|
14
|
+
## Why this exists
|
|
19
15
|
|
|
20
|
-
|
|
16
|
+
Large MCP catalogs are expensive in prompt space.
|
|
21
17
|
|
|
22
|
-
|
|
18
|
+
- **Token waste:** tens of thousands of tokens before first user turn
|
|
19
|
+
- **Reasoning quality loss:** "lost in the middle" on oversized prompts
|
|
20
|
+
- **Higher cost:** more prompt tokens every turn
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
MCP Router applies Anthropic's tool-search pattern so only relevant tools are surfaced when needed.
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
Refs:
|
|
25
|
+
- Tool search / advanced tool use: <https://www.anthropic.com/engineering/advanced-tool-use>
|
|
26
|
+
- Code execution with MCP: <https://www.anthropic.com/engineering/code-execution-with-mcp>
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
---
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
ollama pull embeddinggemma
|
|
30
|
+
## Core model
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
### 1) Index time (`reindex`)
|
|
33
|
+
- Connect to configured MCP servers
|
|
34
|
+
- List tools
|
|
35
|
+
- Embed tool text
|
|
36
|
+
- Store vectors in LanceDB
|
|
37
|
+
- Register toolโserver ownership
|
|
38
|
+
- *(Optional)* generate CLI artifacts via `mcporter generate-cli`
|
|
34
39
|
|
|
35
|
-
### 2
|
|
40
|
+
### 2) Runtime (`mcp_search`)
|
|
41
|
+
- Semantic search over indexed tools
|
|
42
|
+
- Default schema verbosity is adaptive:
|
|
43
|
+
- if `mcporter` is available: compact cards by default
|
|
44
|
+
- if `mcporter` is not available: include JSON params by default
|
|
45
|
+
- Full JSON schema can always be forced with `include_schema=true`
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
### 3) Execute (`mcp_call`)
|
|
48
|
+
- JSON-based execution path (classic MCP params flow)
|
|
39
49
|
|
|
40
|
-
|
|
50
|
+
---
|
|
41
51
|
|
|
42
|
-
|
|
52
|
+
## CLI-first behavior (new)
|
|
43
53
|
|
|
44
|
-
|
|
54
|
+
Router is now optimized for a CLI-first workflow:
|
|
45
55
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
openclaw openclaw-mcp-router reindex
|
|
56
|
+
- Prefer: `mcporter call <server>.<tool> ...`
|
|
57
|
+
- Fallback: `mcp_call(tool_name, params_json)`
|
|
49
58
|
|
|
50
|
-
|
|
59
|
+
`mcp_search` adapts to environment: compact when mcporter is present, schema-forward when it is not (so agents can drive `mcp_call` reliably).
|
|
51
60
|
|
|
52
61
|
---
|
|
53
62
|
|
|
54
|
-
##
|
|
63
|
+
## Quick start
|
|
55
64
|
|
|
56
|
-
|
|
65
|
+
### Prerequisites
|
|
57
66
|
|
|
58
|
-
|
|
67
|
+
```bash
|
|
68
|
+
ollama pull embeddinggemma
|
|
69
|
+
```
|
|
59
70
|
|
|
60
|
-
|
|
71
|
+
### Install
|
|
61
72
|
|
|
62
73
|
```bash
|
|
63
|
-
openclaw openclaw-mcp-router
|
|
74
|
+
openclaw plugins install openclaw-mcp-router
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Setup + index
|
|
64
78
|
|
|
79
|
+
```bash
|
|
80
|
+
openclaw openclaw-mcp-router setup
|
|
81
|
+
openclaw openclaw-mcp-router reindex
|
|
65
82
|
```
|
|
66
83
|
|
|
67
|
-
|
|
84
|
+
The setup wizard auto-detects whether `mcporter` is installed and suggests a sensible default for `mcp_search` schema verbosity.
|
|
85
|
+
|
|
86
|
+
---
|
|
68
87
|
|
|
69
|
-
|
|
88
|
+
## Key configuration
|
|
70
89
|
|
|
71
|
-
|
|
72
|
-
| --- | --- | --- |
|
|
73
|
-
| `topK` | Number of tools returned per search | `5` |
|
|
74
|
-
| `minScore` | Similarity threshold (0.0 - 1.0) | `0.3` |
|
|
75
|
-
| `maxRetries` | Connection attempts for slow servers | `3` |
|
|
90
|
+
In `~/.openclaw/openclaw.json` under `plugins.entries.openclaw-mcp-router.config`:
|
|
76
91
|
|
|
77
92
|
```json5
|
|
78
|
-
// ~/.openclaw/openclaw.json
|
|
79
93
|
{
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
"search": {
|
|
95
|
+
"topK": 5,
|
|
96
|
+
"minScore": 0.3
|
|
97
|
+
// includeParametersDefault optional:
|
|
98
|
+
// true -> always include params
|
|
99
|
+
// false -> always compact
|
|
100
|
+
// unset -> auto (based on mcporter availability)
|
|
101
|
+
},
|
|
102
|
+
"indexer": {
|
|
103
|
+
"connectTimeout": 60000,
|
|
104
|
+
"maxRetries": 3,
|
|
105
|
+
"initialRetryDelay": 2000,
|
|
106
|
+
"maxRetryDelay": 30000,
|
|
107
|
+
"maxChunkChars": 500,
|
|
108
|
+
"overlapChars": 100,
|
|
109
|
+
"generateCliArtifacts": false
|
|
90
110
|
}
|
|
91
111
|
}
|
|
92
|
-
|
|
93
112
|
```
|
|
94
113
|
|
|
95
|
-
|
|
114
|
+
### Notes
|
|
96
115
|
|
|
97
|
-
|
|
116
|
+
- `search.includeParametersDefault` is optional; if omitted, router auto-decides based on mcporter availability.
|
|
117
|
+
- `indexer.generateCliArtifacts=true` enables best-effort per-server `mcporter generate-cli` during reindex.
|
|
118
|
+
- `mcp_call` stays the classic JSON meta-tool (no backend mode flag).
|
|
98
119
|
|
|
99
|
-
|
|
100
|
-
2. **Storage:** These embeddings are stored in a local **LanceDB** instance for sub-millisecond retrieval.
|
|
101
|
-
3. **Runtime Discovery:** * Agent detects a task (e.g., "Analyze this CSV").
|
|
102
|
-
* Agent calls `mcp_search("read or analyze csv files")`.
|
|
103
|
-
* Router returns the `filesystem` tool schema.
|
|
104
|
-
* Agent executes the tool via `mcp_call`.
|
|
120
|
+
---
|
|
105
121
|
|
|
122
|
+
## Server management
|
|
106
123
|
|
|
124
|
+
```bash
|
|
125
|
+
openclaw openclaw-mcp-router control
|
|
126
|
+
openclaw openclaw-mcp-router list
|
|
127
|
+
openclaw openclaw-mcp-router add <name> <command-or-url> [...]
|
|
128
|
+
openclaw openclaw-mcp-router reindex
|
|
129
|
+
```
|
|
107
130
|
|
|
108
131
|
---
|
|
109
132
|
|
|
110
|
-
##
|
|
133
|
+
## MCPorter inspiration
|
|
134
|
+
|
|
135
|
+
Huge thanks to **@steipete** and [mcporter](https://github.com/steipete/mcporter) for the CLI-first MCP execution model inspiration.
|
|
136
|
+
|
|
137
|
+
---
|
|
111
138
|
|
|
112
|
-
|
|
139
|
+
## Documentation
|
|
113
140
|
|
|
114
|
-
|
|
115
|
-
|
|
141
|
+
- Architecture + flow details: `docs/CLI_FIRST_WORKFLOW.md`
|
|
142
|
+
- Plugin config schema: `openclaw.plugin.json`
|
|
143
|
+
- Skill usage examples: `skills/mcp-router/`
|
|
116
144
|
|
|
117
145
|
---
|
|
118
146
|
|
|
119
|
-
##
|
|
147
|
+
## Contributing
|
|
120
148
|
|
|
121
|
-
|
|
149
|
+
PRs welcome โ especially around:
|
|
150
|
+
- better reranking
|
|
151
|
+
- hybrid retrieval (vector + lexical)
|
|
122
152
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
153
|
+
PR hygiene:
|
|
154
|
+
- keep README + docs in sync for every behavior/config/workflow change
|
|
155
|
+
- keep `skills/` guidance in sync when user-facing behavior changes
|
|
156
|
+
- run test suite before opening PR
|
|
126
157
|
|
|
127
|
-
##
|
|
158
|
+
## License
|
|
128
159
|
|
|
129
|
-
|
|
160
|
+
MIT
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# CLI-First Workflow (MCP Router + MCPorter)
|
|
2
|
+
|
|
3
|
+
This document explains the intended execution model:
|
|
4
|
+
|
|
5
|
+
1. Reindex tools + metadata
|
|
6
|
+
2. Search tools with adaptive schema verbosity
|
|
7
|
+
3. Prefer CLI calls when available
|
|
8
|
+
4. Use `mcp_call` as classic JSON fallback
|
|
9
|
+
|
|
10
|
+
## Reindex behavior
|
|
11
|
+
|
|
12
|
+
During `openclaw openclaw-mcp-router reindex`:
|
|
13
|
+
|
|
14
|
+
- Router connects to enabled MCP servers
|
|
15
|
+
- Lists tools
|
|
16
|
+
- Chunks/embeds descriptions
|
|
17
|
+
- Stores vectors + metadata in LanceDB
|
|
18
|
+
|
|
19
|
+
Optional:
|
|
20
|
+
- If `indexer.generateCliArtifacts=true`, router runs best-effort `mcporter generate-cli` per server.
|
|
21
|
+
- Generation failures do **not** block indexing.
|
|
22
|
+
|
|
23
|
+
## `mcp_search` behavior
|
|
24
|
+
|
|
25
|
+
`mcp_search` supports adaptive defaults:
|
|
26
|
+
|
|
27
|
+
- If `mcporter` is installed: default to compact cards (save tokens)
|
|
28
|
+
- If `mcporter` is not installed: include JSON params by default (so agents can call `mcp_call` reliably)
|
|
29
|
+
|
|
30
|
+
Overrides:
|
|
31
|
+
- request-level: `include_schema=true|false`
|
|
32
|
+
- config-level: `search.includeParametersDefault=true|false`
|
|
33
|
+
- if unset, auto mode is used
|
|
34
|
+
|
|
35
|
+
## `mcp_call` behavior
|
|
36
|
+
|
|
37
|
+
`mcp_call` remains the original JSON meta-tool path:
|
|
38
|
+
|
|
39
|
+
- resolve tool owner by registry
|
|
40
|
+
- open MCP connection
|
|
41
|
+
- call tool with `params_json`
|
|
42
|
+
- return content/errors
|
|
43
|
+
|
|
44
|
+
No backend mode flag is required.
|
|
45
|
+
|
|
46
|
+
## Recommended config
|
|
47
|
+
|
|
48
|
+
```json5
|
|
49
|
+
{
|
|
50
|
+
"plugins": {
|
|
51
|
+
"entries": {
|
|
52
|
+
"openclaw-mcp-router": {
|
|
53
|
+
"enabled": true,
|
|
54
|
+
"config": {
|
|
55
|
+
"search": {
|
|
56
|
+
"topK": 5,
|
|
57
|
+
"minScore": 0.3
|
|
58
|
+
// includeParametersDefault optional (true|false)
|
|
59
|
+
},
|
|
60
|
+
"indexer": {
|
|
61
|
+
"generateCliArtifacts": true
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Acknowledgement
|
|
71
|
+
|
|
72
|
+
Special thanks to **@steipete** and **MCPorter**:
|
|
73
|
+
<https://github.com/steipete/mcporter>
|
|
74
|
+
|
|
75
|
+
Anthropic reference:
|
|
76
|
+
<https://www.anthropic.com/engineering/code-execution-with-mcp>
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-mcp-router",
|
|
3
3
|
"name": "MCP Router",
|
|
4
|
-
"description": "Semantic search across MCP tool catalogs. Reduces context bloat from ~77k
|
|
4
|
+
"description": "Semantic search across MCP tool catalogs. Reduces context bloat from ~77k\u21928.7k tokens by dynamically surfacing only relevant tools.",
|
|
5
5
|
"configSchema": {
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
@@ -12,14 +12,45 @@
|
|
|
12
12
|
"additionalProperties": {
|
|
13
13
|
"type": "object",
|
|
14
14
|
"properties": {
|
|
15
|
-
"command":
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
"command": {
|
|
16
|
+
"type": "string"
|
|
17
|
+
},
|
|
18
|
+
"args": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"items": {
|
|
21
|
+
"type": "string"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"env": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"additionalProperties": {
|
|
27
|
+
"type": "string"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"url": {
|
|
31
|
+
"type": "string"
|
|
32
|
+
},
|
|
33
|
+
"serverUrl": {
|
|
34
|
+
"type": "string"
|
|
35
|
+
},
|
|
36
|
+
"type": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"enum": [
|
|
39
|
+
"stdio",
|
|
40
|
+
"sse",
|
|
41
|
+
"http"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
"headers": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"additionalProperties": {
|
|
47
|
+
"type": "string"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"timeout": {
|
|
51
|
+
"type": "number",
|
|
52
|
+
"description": "Per-server connect timeout in ms; overrides indexer.connectTimeout"
|
|
53
|
+
}
|
|
23
54
|
}
|
|
24
55
|
}
|
|
25
56
|
},
|
|
@@ -32,16 +63,50 @@
|
|
|
32
63
|
"description": "Legacy server array format. Prefer mcpServers dict.",
|
|
33
64
|
"items": {
|
|
34
65
|
"type": "object",
|
|
35
|
-
"required": [
|
|
66
|
+
"required": [
|
|
67
|
+
"name",
|
|
68
|
+
"transport"
|
|
69
|
+
],
|
|
36
70
|
"properties": {
|
|
37
|
-
"name":
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
71
|
+
"name": {
|
|
72
|
+
"type": "string"
|
|
73
|
+
},
|
|
74
|
+
"transport": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"enum": [
|
|
77
|
+
"stdio",
|
|
78
|
+
"sse",
|
|
79
|
+
"http"
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
"command": {
|
|
83
|
+
"type": "string"
|
|
84
|
+
},
|
|
85
|
+
"args": {
|
|
86
|
+
"type": "array",
|
|
87
|
+
"items": {
|
|
88
|
+
"type": "string"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"env": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"additionalProperties": {
|
|
94
|
+
"type": "string"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"url": {
|
|
98
|
+
"type": "string"
|
|
99
|
+
},
|
|
100
|
+
"headers": {
|
|
101
|
+
"type": "object",
|
|
102
|
+
"additionalProperties": {
|
|
103
|
+
"type": "string"
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"timeout": {
|
|
107
|
+
"type": "number",
|
|
108
|
+
"description": "Per-server connect timeout in ms; overrides indexer.connectTimeout"
|
|
109
|
+
}
|
|
45
110
|
}
|
|
46
111
|
}
|
|
47
112
|
},
|
|
@@ -49,34 +114,98 @@
|
|
|
49
114
|
"type": "object",
|
|
50
115
|
"description": "Indexer retry and timeout settings",
|
|
51
116
|
"properties": {
|
|
52
|
-
"connectTimeout":
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
|
|
117
|
+
"connectTimeout": {
|
|
118
|
+
"type": "number",
|
|
119
|
+
"description": "Per-server default connect timeout in ms (default: 60000)"
|
|
120
|
+
},
|
|
121
|
+
"maxRetries": {
|
|
122
|
+
"type": "number",
|
|
123
|
+
"minimum": 0,
|
|
124
|
+
"description": "Retry attempts per server, 0 = no retry (default: 3)"
|
|
125
|
+
},
|
|
126
|
+
"initialRetryDelay": {
|
|
127
|
+
"type": "number",
|
|
128
|
+
"description": "Initial backoff delay in ms (default: 2000)"
|
|
129
|
+
},
|
|
130
|
+
"maxRetryDelay": {
|
|
131
|
+
"type": "number",
|
|
132
|
+
"description": "Max backoff cap in ms (default: 30000)"
|
|
133
|
+
},
|
|
134
|
+
"maxChunkChars": {
|
|
135
|
+
"type": "number",
|
|
136
|
+
"minimum": 0,
|
|
137
|
+
"description": "Max chars per chunk for long tool descriptions. 0 = disable chunking (default: 500)"
|
|
138
|
+
},
|
|
139
|
+
"overlapChars": {
|
|
140
|
+
"type": "number",
|
|
141
|
+
"minimum": 0,
|
|
142
|
+
"description": "Overlap chars between adjacent chunks (default: 100)"
|
|
143
|
+
},
|
|
144
|
+
"generateCliArtifacts": {
|
|
145
|
+
"type": "boolean",
|
|
146
|
+
"description": "Generate mcporter CLI artifacts during reindex (default: false)"
|
|
147
|
+
}
|
|
58
148
|
}
|
|
59
149
|
},
|
|
60
150
|
"embedding": {
|
|
61
151
|
"type": "object",
|
|
62
152
|
"properties": {
|
|
63
|
-
"provider": {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
153
|
+
"provider": {
|
|
154
|
+
"type": "string",
|
|
155
|
+
"enum": [
|
|
156
|
+
"openai",
|
|
157
|
+
"gemini",
|
|
158
|
+
"voyage",
|
|
159
|
+
"mistral",
|
|
160
|
+
"ollama"
|
|
161
|
+
]
|
|
162
|
+
},
|
|
163
|
+
"model": {
|
|
164
|
+
"type": "string"
|
|
165
|
+
},
|
|
166
|
+
"baseUrl": {
|
|
167
|
+
"type": "string"
|
|
168
|
+
},
|
|
169
|
+
"url": {
|
|
170
|
+
"type": "string",
|
|
171
|
+
"description": "Deprecated: use baseUrl. Old Ollama URL (gets /v1 appended)."
|
|
172
|
+
},
|
|
173
|
+
"apiKey": {
|
|
174
|
+
"type": "string"
|
|
175
|
+
},
|
|
176
|
+
"headers": {
|
|
177
|
+
"type": "object",
|
|
178
|
+
"additionalProperties": {
|
|
179
|
+
"type": "string"
|
|
180
|
+
}
|
|
181
|
+
}
|
|
69
182
|
}
|
|
70
183
|
},
|
|
71
184
|
"vectorDb": {
|
|
72
185
|
"type": "object",
|
|
73
|
-
"properties": {
|
|
186
|
+
"properties": {
|
|
187
|
+
"path": {
|
|
188
|
+
"type": "string"
|
|
189
|
+
}
|
|
190
|
+
}
|
|
74
191
|
},
|
|
75
192
|
"search": {
|
|
76
193
|
"type": "object",
|
|
77
194
|
"properties": {
|
|
78
|
-
"topK":
|
|
79
|
-
|
|
195
|
+
"topK": {
|
|
196
|
+
"type": "number",
|
|
197
|
+
"minimum": 1,
|
|
198
|
+
"maximum": 20
|
|
199
|
+
},
|
|
200
|
+
"minScore": {
|
|
201
|
+
"type": "number",
|
|
202
|
+
"minimum": 0,
|
|
203
|
+
"maximum": 1
|
|
204
|
+
},
|
|
205
|
+
"includeParametersDefault": {
|
|
206
|
+
"type": "boolean",
|
|
207
|
+
"description": "Include full input schema by default in mcp_search results. If omitted, defaults automatically based on mcporter availability."
|
|
208
|
+
}
|
|
80
209
|
}
|
|
81
210
|
}
|
|
82
211
|
}
|
package/package.json
CHANGED
|
@@ -21,13 +21,23 @@ If a relevant tool exists, use it with `mcp_call`.
|
|
|
21
21
|
- Use an action-oriented query: `"create github pull request"`, `"query postgres"`, `"send slack message"`.
|
|
22
22
|
2. **Select tool**
|
|
23
23
|
- Prefer best intent match + feasible required params.
|
|
24
|
-
3. **
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
3. **Decide execution path**
|
|
25
|
+
- If `mcporter` is available, prefer CLI invocation style shown in search hints.
|
|
26
|
+
- Otherwise, use `mcp_call` with JSON params.
|
|
27
|
+
4. **Read schema when needed**
|
|
28
|
+
- Use `include_schema=true` on `mcp_search` when full parameter detail is required.
|
|
28
29
|
5. **Recover on failure**
|
|
29
30
|
- Fix schema/type mismatch or re-search with rewritten query.
|
|
30
31
|
|
|
32
|
+
## Adaptive `mcp_search` Defaults
|
|
33
|
+
|
|
34
|
+
- Default schema verbosity can be auto-configured by environment:
|
|
35
|
+
- `mcporter` installed โ compact search cards by default
|
|
36
|
+
- `mcporter` not installed โ include parameter schema by default
|
|
37
|
+
- Overrides:
|
|
38
|
+
- Per call: `include_schema=true|false`
|
|
39
|
+
- Config: `search.includeParametersDefault=true|false`
|
|
40
|
+
|
|
31
41
|
## Query Rewrite Ladder (Deterministic)
|
|
32
42
|
|
|
33
43
|
If search quality is poor, retry in this order:
|
|
@@ -50,7 +60,7 @@ When multiple tools match, rank by:
|
|
|
50
60
|
|
|
51
61
|
## `mcp_call` Parameter Checklist
|
|
52
62
|
|
|
53
|
-
`params_json` must be a **JSON string**.
|
|
63
|
+
`mcp_call` is the classic MCP JSON meta-tool. `params_json` must be a **JSON string**.
|
|
54
64
|
|
|
55
65
|
- Include all required fields.
|
|
56
66
|
- Match exact types (`42` vs `"42"`, `true` vs `"true"`).
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mcp-server-manager
|
|
3
|
+
description: Manage MCP servers for openclaw-mcp-router (add/list/enable/disable/remove/reindex/setup/control), including choosing openclaw.json vs ~/.openclaw/openclaw-mcp-router/.mcp.json and validating server state after changes. Use when the user asks to add new MCP capabilities, troubleshoot missing tools, rotate credentials/env vars, or maintain MCP server inventory.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# MCP Server Manager
|
|
7
|
+
|
|
8
|
+
Manage MCP server lifecycle through `openclaw-mcp-router` commands first; edit config files directly only when CLI paths cannot express the change.
|
|
9
|
+
|
|
10
|
+
## Command Surface
|
|
11
|
+
|
|
12
|
+
Use these exact commands:
|
|
13
|
+
|
|
14
|
+
- `openclaw openclaw-mcp-router setup`
|
|
15
|
+
- `openclaw openclaw-mcp-router control`
|
|
16
|
+
- `openclaw openclaw-mcp-router add <name> <command-or-url> [args...] [--transport stdio|sse|http] [--env KEY=VALUE ...] [--timeout <ms>] [--file]`
|
|
17
|
+
- `openclaw openclaw-mcp-router list`
|
|
18
|
+
- `openclaw openclaw-mcp-router enable <name>`
|
|
19
|
+
- `openclaw openclaw-mcp-router disable <name>`
|
|
20
|
+
- `openclaw openclaw-mcp-router remove <name>`
|
|
21
|
+
- `openclaw openclaw-mcp-router reindex [--server <name>]`
|
|
22
|
+
|
|
23
|
+
## Source of Truth and Precedence
|
|
24
|
+
|
|
25
|
+
Server definitions can come from two places:
|
|
26
|
+
|
|
27
|
+
1. inline plugin config (`plugins.entries.openclaw-mcp-router.config.mcpServers` in `~/.openclaw/openclaw.json`)
|
|
28
|
+
2. file-based config (`~/.openclaw/openclaw-mcp-router/.mcp.json` by default, or configured `mcpServersFile`)
|
|
29
|
+
|
|
30
|
+
Resolution rules:
|
|
31
|
+
|
|
32
|
+
- Both sources are merged.
|
|
33
|
+
- Name collisions are resolved with inline `mcpServers` winning over file-based entries.
|
|
34
|
+
- Disabled servers (`disabled: true`) are skipped during indexing.
|
|
35
|
+
|
|
36
|
+
## Standard Workflow
|
|
37
|
+
|
|
38
|
+
1. Inspect current state:
|
|
39
|
+
- `openclaw openclaw-mcp-router list`
|
|
40
|
+
2. Apply change (add/enable/disable/remove).
|
|
41
|
+
3. Reindex:
|
|
42
|
+
- Full: `openclaw openclaw-mcp-router reindex`
|
|
43
|
+
- Single server: `openclaw openclaw-mcp-router reindex --server <name>`
|
|
44
|
+
4. Verify:
|
|
45
|
+
- `openclaw openclaw-mcp-router list`
|
|
46
|
+
- confirm status is `ok` and tool count is non-zero when expected.
|
|
47
|
+
|
|
48
|
+
## Add Server Patterns
|
|
49
|
+
|
|
50
|
+
### Stdio server (default transport)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
openclaw openclaw-mcp-router add filesystem npx -y @modelcontextprotocol/server-filesystem /path/to/root
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### HTTP/SSE server
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
openclaw openclaw-mcp-router add notion https://mcp.example.com --transport http
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Store in `.mcp.json` instead of `openclaw.json`
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
openclaw openclaw-mcp-router add github npx -y @modelcontextprotocol/server-github --env GITHUB_TOKEN=${GITHUB_TOKEN} --file
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Operational Notes
|
|
69
|
+
|
|
70
|
+
- Always run `reindex` after add/remove and after most enable/disable changes.
|
|
71
|
+
- If a server fails to connect, retry with larger timeout (for slow startup servers):
|
|
72
|
+
- `--timeout 120000`
|
|
73
|
+
- For env updates, re-run `add` with the same name (entry is replaced), then reindex.
|
|
74
|
+
- Prefer `--file` when keeping credentials and frequently changing servers outside `openclaw.json`.
|
|
75
|
+
|
|
76
|
+
## Troubleshooting Quick Checks
|
|
77
|
+
|
|
78
|
+
- No results in `mcp_search`: verify server is enabled and indexed.
|
|
79
|
+
- `list` shows `failed`: inspect endpoint/command/env and rerun reindex.
|
|
80
|
+
- Server missing: check whether it exists in inline config vs `.mcp.json` and whether an inline entry with same name is overriding it.
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
## Setup Behavior (mcporter-aware)
|
|
84
|
+
|
|
85
|
+
- During `openclaw openclaw-mcp-router setup`, detect whether `mcporter` is installed and suggest `mcp_search` schema defaults accordingly.
|
|
86
|
+
- First-install guidance:
|
|
87
|
+
- no `mcporter`: keep params visible by default (`search.includeParametersDefault=true`)
|
|
88
|
+
- with `mcporter`: prefer compact cards by default (`search.includeParametersDefault=false`)
|
|
89
|
+
- This can still be overridden by users later in plugin config.
|
|
Binary file
|
|
Binary file
|
package/src/config.ts
CHANGED
|
@@ -41,13 +41,16 @@ export type IndexerConfig = {
|
|
|
41
41
|
maxChunkChars: number;
|
|
42
42
|
/** Overlap characters between adjacent chunks (default: 100) */
|
|
43
43
|
overlapChars: number;
|
|
44
|
+
/** Generate mcporter CLI artifacts during reindex (default: false). */
|
|
45
|
+
generateCliArtifacts: boolean;
|
|
44
46
|
};
|
|
45
47
|
|
|
48
|
+
|
|
46
49
|
export type McpRouterConfig = {
|
|
47
50
|
servers: McpServerConfig[];
|
|
48
51
|
embedding: EmbeddingConfig;
|
|
49
52
|
vectorDb: { path: string };
|
|
50
|
-
search: { topK: number; minScore: number };
|
|
53
|
+
search: { topK: number; minScore: number; includeParametersDefault?: boolean };
|
|
51
54
|
indexer: IndexerConfig;
|
|
52
55
|
};
|
|
53
56
|
|
|
@@ -356,6 +359,7 @@ export function parseConfig(raw: unknown, opts?: ParseConfigOpts): McpRouterConf
|
|
|
356
359
|
const search = {
|
|
357
360
|
topK: typeof srchRaw.topK === "number" ? Math.min(20, Math.max(1, srchRaw.topK)) : 5,
|
|
358
361
|
minScore: typeof srchRaw.minScore === "number" ? srchRaw.minScore : 0.3,
|
|
362
|
+
includeParametersDefault: typeof srchRaw.includeParametersDefault === "boolean" ? srchRaw.includeParametersDefault : undefined,
|
|
359
363
|
};
|
|
360
364
|
|
|
361
365
|
// โโ indexer defaults โโ
|
|
@@ -367,6 +371,7 @@ export function parseConfig(raw: unknown, opts?: ParseConfigOpts): McpRouterConf
|
|
|
367
371
|
maxRetryDelay: typeof idxRaw.maxRetryDelay === "number" ? idxRaw.maxRetryDelay : 30_000,
|
|
368
372
|
maxChunkChars: typeof idxRaw.maxChunkChars === "number" ? Math.max(0, idxRaw.maxChunkChars) : 500,
|
|
369
373
|
overlapChars: typeof idxRaw.overlapChars === "number" ? Math.max(0, idxRaw.overlapChars) : 100,
|
|
374
|
+
generateCliArtifacts: typeof idxRaw.generateCliArtifacts === "boolean" ? idxRaw.generateCliArtifacts : false,
|
|
370
375
|
};
|
|
371
376
|
|
|
372
377
|
return { servers, embedding, vectorDb, search, indexer };
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
4
5
|
import { parseConfig } from "./config.js";
|
|
@@ -10,6 +11,16 @@ import { createMcpCallTool } from "./tools/mcp-call-tool.js";
|
|
|
10
11
|
import { createMcpSearchTool } from "./tools/mcp-search-tool.js";
|
|
11
12
|
import { McpToolVectorStore } from "./vector-store.js";
|
|
12
13
|
|
|
14
|
+
|
|
15
|
+
function detectMcporterInstalled(): boolean {
|
|
16
|
+
try {
|
|
17
|
+
const r = spawnSync("mcporter", ["--version"], { stdio: "ignore" });
|
|
18
|
+
return r.status === 0;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
const mcpRouterPlugin = {
|
|
14
25
|
id: EXTENSION_ID,
|
|
15
26
|
name: "OpenClaw MCP Router",
|
|
@@ -179,12 +190,14 @@ const mcpRouterPlugin = {
|
|
|
179
190
|
}
|
|
180
191
|
|
|
181
192
|
// Register tools as optional so the agent only sees them when alsoAllow is set
|
|
193
|
+
const hasMcporter = detectMcporterInstalled();
|
|
194
|
+
|
|
182
195
|
api.registerTool(
|
|
183
|
-
createMcpSearchTool({ store, embeddings, cfg: cfg.search }),
|
|
196
|
+
createMcpSearchTool({ store, embeddings, cfg: cfg.search, hasMcporter }) as never,
|
|
184
197
|
{ optional: true },
|
|
185
198
|
);
|
|
186
199
|
api.registerTool(
|
|
187
|
-
createMcpCallTool({ registry, logger: api.logger }),
|
|
200
|
+
createMcpCallTool({ registry, logger: api.logger }) as never,
|
|
188
201
|
{ optional: true },
|
|
189
202
|
);
|
|
190
203
|
|
package/src/indexer.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import type { Embeddings } from "./embeddings.js";
|
|
2
5
|
import type { McpRegistry } from "./mcp-registry.js";
|
|
3
6
|
import { McpClient } from "./mcp-client.js";
|
|
@@ -48,6 +51,49 @@ export function abortableDelay(ms: number, signal?: AbortSignal): Promise<void>
|
|
|
48
51
|
});
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
|
|
55
|
+
async function generateCliForServer(params: {
|
|
56
|
+
serverCfg: McpServerConfig;
|
|
57
|
+
cfg: McpRouterConfig;
|
|
58
|
+
logger: IndexerLogger;
|
|
59
|
+
}): Promise<void> {
|
|
60
|
+
const { serverCfg, cfg, logger } = params;
|
|
61
|
+
// Best-effort CLI artifact generation inspired by mcporter.
|
|
62
|
+
// Stores generated artifacts alongside router state.
|
|
63
|
+
const outDir = path.join(path.dirname(cfg.vectorDb.path), "generated-clis");
|
|
64
|
+
const outFile = path.join(outDir, `${serverCfg.name}.ts`);
|
|
65
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
66
|
+
|
|
67
|
+
const args = ["-y", "mcporter", "generate-cli"];
|
|
68
|
+
if (serverCfg.transport === "stdio") {
|
|
69
|
+
if (!serverCfg.command) return;
|
|
70
|
+
const cmd = [serverCfg.command, ...(serverCfg.args ?? [])].join(" ");
|
|
71
|
+
args.push("--command", cmd);
|
|
72
|
+
} else if (serverCfg.url) {
|
|
73
|
+
args.push("--server", serverCfg.url);
|
|
74
|
+
} else {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
args.push("--name", serverCfg.name, "--output", outFile);
|
|
79
|
+
|
|
80
|
+
await new Promise<void>((resolve) => {
|
|
81
|
+
const child = spawn("npx", args, { stdio: ["ignore", "pipe", "pipe"], env: process.env });
|
|
82
|
+
let stderr = "";
|
|
83
|
+
child.stderr.on("data", (d) => (stderr += String(d)));
|
|
84
|
+
child.on("close", (code) => {
|
|
85
|
+
if (code === 0) {
|
|
86
|
+
logger.info(`${EXTENSION_ID}: generated CLI artifact for server "${serverCfg.name}" at ${outFile}`);
|
|
87
|
+
} else {
|
|
88
|
+
logger.warn(
|
|
89
|
+
`${EXTENSION_ID}: failed to generate CLI artifact for "${serverCfg.name}" (best-effort): ${stderr.trim() || `exit ${String(code)}`}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
resolve();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
51
97
|
/**
|
|
52
98
|
* Connect to all configured MCP servers in parallel, list their tools,
|
|
53
99
|
* embed each tool description, and upsert into the vector store.
|
|
@@ -131,6 +177,12 @@ async function indexServer(params: {
|
|
|
131
177
|
await client.connect({ signal, timeout: connectTimeout });
|
|
132
178
|
const tools = await client.listTools();
|
|
133
179
|
|
|
180
|
+
// Optional: best-effort generation of per-server CLI wrapper via mcporter.
|
|
181
|
+
// Indexing continues even if generation fails.
|
|
182
|
+
if (cfg.indexer.generateCliArtifacts) {
|
|
183
|
+
await generateCliForServer({ serverCfg, cfg, logger });
|
|
184
|
+
}
|
|
185
|
+
|
|
134
186
|
let indexed = 0;
|
|
135
187
|
let failed = 0;
|
|
136
188
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { cancel, confirm, intro, isCancel, outro, select, text } from "@clack/prompts";
|
|
2
3
|
import { parseConfig, type McpServerConfig, type McpTransportKind } from "../config.js";
|
|
3
4
|
import {
|
|
@@ -10,6 +11,16 @@ import {
|
|
|
10
11
|
} from "./config-writer.js";
|
|
11
12
|
import { CMD_REINDEX, EXTENSION_ID } from "../constants.js";
|
|
12
13
|
|
|
14
|
+
|
|
15
|
+
function detectMcporterInstalled(): boolean {
|
|
16
|
+
try {
|
|
17
|
+
const r = spawnSync("mcporter", ["--version"], { stdio: "ignore" });
|
|
18
|
+
return r.status === 0;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
function abortIfCancel(value: unknown): void {
|
|
14
25
|
if (isCancel(value)) {
|
|
15
26
|
cancel("Setup cancelled.");
|
|
@@ -167,7 +178,22 @@ export async function runSetupCommand(): Promise<void> {
|
|
|
167
178
|
overlapChars = parseInt(rawOverlapChars as string, 10) || 100;
|
|
168
179
|
}
|
|
169
180
|
|
|
170
|
-
// Step 4:
|
|
181
|
+
// Step 4: Search verbosity defaults
|
|
182
|
+
// First install assumption: no mcporter => include params by default.
|
|
183
|
+
const hasMcporter = detectMcporterInstalled();
|
|
184
|
+
const schemaPreference = await confirm({
|
|
185
|
+
message: hasMcporter
|
|
186
|
+
? "mcporter detected. Use compact mcp_search output by default?"
|
|
187
|
+
: "mcporter not detected. Keep full params in mcp_search by default?",
|
|
188
|
+
initialValue: true,
|
|
189
|
+
});
|
|
190
|
+
abortIfCancel(schemaPreference);
|
|
191
|
+
|
|
192
|
+
const includeParametersDefault = hasMcporter
|
|
193
|
+
? !Boolean(schemaPreference)
|
|
194
|
+
: Boolean(schemaPreference);
|
|
195
|
+
|
|
196
|
+
// Step 5: Write config
|
|
171
197
|
// Build mcpServers dict (key = server name, value = entry without name field)
|
|
172
198
|
const mcpServers: Record<string, unknown> = {};
|
|
173
199
|
for (const srv of servers) {
|
|
@@ -181,7 +207,7 @@ export async function runSetupCommand(): Promise<void> {
|
|
|
181
207
|
model: embeddingModel,
|
|
182
208
|
url: ollamaUrl as string,
|
|
183
209
|
},
|
|
184
|
-
search: { topK, minScore },
|
|
210
|
+
search: { topK, minScore, includeParametersDefault },
|
|
185
211
|
indexer: { maxChunkChars, overlapChars },
|
|
186
212
|
};
|
|
187
213
|
|
package/src/test/config.test.ts
CHANGED
|
@@ -540,6 +540,7 @@ describe("parseConfig", () => {
|
|
|
540
540
|
maxRetryDelay: 30_000,
|
|
541
541
|
maxChunkChars: 500,
|
|
542
542
|
overlapChars: 100,
|
|
543
|
+
generateCliArtifacts: false,
|
|
543
544
|
});
|
|
544
545
|
});
|
|
545
546
|
|
|
@@ -561,6 +562,7 @@ describe("parseConfig", () => {
|
|
|
561
562
|
maxRetryDelay: 15_000,
|
|
562
563
|
maxChunkChars: 4000,
|
|
563
564
|
overlapChars: 400,
|
|
565
|
+
generateCliArtifacts: false,
|
|
564
566
|
});
|
|
565
567
|
});
|
|
566
568
|
|
package/src/test/indexer.test.ts
CHANGED
|
@@ -19,7 +19,7 @@ const mockCfg: McpRouterConfig = {
|
|
|
19
19
|
embedding: { provider: "ollama", model: "embeddinggemma", baseUrl: "http://localhost:11434/v1" },
|
|
20
20
|
vectorDb: { path: "/tmp/test-lancedb" },
|
|
21
21
|
search: { topK: 5, minScore: 0.3 },
|
|
22
|
-
indexer: { connectTimeout: 60_000, maxRetries: 3, initialRetryDelay: 2_000, maxRetryDelay: 30_000, maxChunkChars: 500, overlapChars: 100 },
|
|
22
|
+
indexer: { connectTimeout: 60_000, maxRetries: 3, initialRetryDelay: 2_000, maxRetryDelay: 30_000, maxChunkChars: 500, overlapChars: 100, generateCliArtifacts: false },
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
function makeStore() {
|
|
@@ -23,6 +23,7 @@ function makeTool(storeResults = makeStore()) {
|
|
|
23
23
|
store: storeResults as never,
|
|
24
24
|
embeddings: makeEmbeddings() as never,
|
|
25
25
|
cfg: { topK: 5, minScore: 0.3 },
|
|
26
|
+
hasMcporter: false,
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -43,6 +44,7 @@ describe(`${TOOL_MCP_SEARCH} tool`, () => {
|
|
|
43
44
|
store: store as never,
|
|
44
45
|
embeddings: makeEmbeddings() as never,
|
|
45
46
|
cfg: { topK: 5, minScore: 0.3 },
|
|
47
|
+
hasMcporter: false,
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
const result = await tool.execute("id", { query: "list files" });
|
|
@@ -69,6 +71,7 @@ describe(`${TOOL_MCP_SEARCH} tool`, () => {
|
|
|
69
71
|
store: store as never,
|
|
70
72
|
embeddings: makeEmbeddings() as never,
|
|
71
73
|
cfg: { topK: 5, minScore: 0.3 },
|
|
74
|
+
hasMcporter: false,
|
|
72
75
|
});
|
|
73
76
|
|
|
74
77
|
const result = await tool.execute("id", { query: "read file" });
|
|
@@ -84,6 +87,7 @@ describe(`${TOOL_MCP_SEARCH} tool`, () => {
|
|
|
84
87
|
store: store as never,
|
|
85
88
|
embeddings: makeEmbeddings() as never,
|
|
86
89
|
cfg: { topK: 5, minScore: 0.3 },
|
|
90
|
+
hasMcporter: false,
|
|
87
91
|
});
|
|
88
92
|
|
|
89
93
|
await tool.execute("id", { query: "test", limit: 999 });
|
|
@@ -115,9 +119,10 @@ describe(`${TOOL_MCP_SEARCH} tool`, () => {
|
|
|
115
119
|
store: store as never,
|
|
116
120
|
embeddings: makeEmbeddings() as never,
|
|
117
121
|
cfg: { topK: 5, minScore: 0.3 },
|
|
122
|
+
hasMcporter: false,
|
|
118
123
|
});
|
|
119
124
|
|
|
120
|
-
const result = await tool.execute("id", { query: "test" });
|
|
125
|
+
const result = await tool.execute("id", { query: "test", include_schema: true });
|
|
121
126
|
expect((result.content[0] as { text: string }).text).toContain("truncated");
|
|
122
127
|
});
|
|
123
128
|
|
|
@@ -127,6 +132,7 @@ describe(`${TOOL_MCP_SEARCH} tool`, () => {
|
|
|
127
132
|
store: store as never,
|
|
128
133
|
embeddings: makeEmbeddings() as never,
|
|
129
134
|
cfg: { topK: 5, minScore: 0.3 },
|
|
135
|
+
hasMcporter: false,
|
|
130
136
|
});
|
|
131
137
|
|
|
132
138
|
await tool.execute("id", { query: "test", limit: 10 });
|
|
@@ -140,6 +146,7 @@ describe(`${TOOL_MCP_SEARCH} tool`, () => {
|
|
|
140
146
|
store: store as never,
|
|
141
147
|
embeddings: makeEmbeddings() as never,
|
|
142
148
|
cfg: { topK: 5, minScore: 0.3 },
|
|
149
|
+
hasMcporter: false,
|
|
143
150
|
});
|
|
144
151
|
|
|
145
152
|
await tool.execute("id", { query: "test", limit: 999 });
|
|
@@ -188,6 +195,7 @@ describe(`${TOOL_MCP_SEARCH} tool`, () => {
|
|
|
188
195
|
store: store as never,
|
|
189
196
|
embeddings: makeEmbeddings() as never,
|
|
190
197
|
cfg: { topK: 5, minScore: 0.3 },
|
|
198
|
+
hasMcporter: false,
|
|
191
199
|
});
|
|
192
200
|
|
|
193
201
|
const result = await tool.execute("id", { query: "read file" });
|
|
@@ -209,6 +217,7 @@ describe(`${TOOL_MCP_SEARCH} tool`, () => {
|
|
|
209
217
|
store: makeStore() as never,
|
|
210
218
|
embeddings: embeddings as never,
|
|
211
219
|
cfg: { topK: 5, minScore: 0.3 },
|
|
220
|
+
hasMcporter: false,
|
|
212
221
|
});
|
|
213
222
|
|
|
214
223
|
const result = await tool.execute("id", { query: "files" });
|
|
@@ -6,7 +6,8 @@ import type { McpToolVectorStore } from "../vector-store.js";
|
|
|
6
6
|
type SearchDeps = {
|
|
7
7
|
store: McpToolVectorStore;
|
|
8
8
|
embeddings: Embeddings;
|
|
9
|
-
cfg: { topK: number; minScore: number };
|
|
9
|
+
cfg: { topK: number; minScore: number; includeParametersDefault?: boolean };
|
|
10
|
+
hasMcporter: boolean;
|
|
10
11
|
};
|
|
11
12
|
|
|
12
13
|
/** Extract a string param tolerating both camelCase and snake_case keys. */
|
|
@@ -15,9 +16,32 @@ function readStringParam(params: Record<string, unknown>, key: string): string |
|
|
|
15
16
|
return typeof val === "string" ? val : undefined;
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
function readBoolParam(params: Record<string, unknown>, key: string): boolean | undefined {
|
|
20
|
+
const val = params[key] ?? params[key.replace(/([A-Z])/g, "_$1").toLowerCase()];
|
|
21
|
+
return typeof val === "boolean" ? val : undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildSignature(toolName: string, paramsJson: string): string {
|
|
25
|
+
try {
|
|
26
|
+
const schema = JSON.parse(paramsJson) as {
|
|
27
|
+
properties?: Record<string, { type?: string }>;
|
|
28
|
+
required?: string[];
|
|
29
|
+
};
|
|
30
|
+
const props = schema.properties ?? {};
|
|
31
|
+
const required = new Set(schema.required ?? []);
|
|
32
|
+
const parts = Object.entries(props).map(([name, def]) => {
|
|
33
|
+
const type = def?.type ?? "unknown";
|
|
34
|
+
return required.has(name) ? `${name}: ${type}` : `${name}?: ${type}`;
|
|
35
|
+
});
|
|
36
|
+
return `${toolName}(${parts.join(", ")})`;
|
|
37
|
+
} catch {
|
|
38
|
+
return `${toolName}(...)`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
18
42
|
/**
|
|
19
43
|
* mcp_search โ semantic search over indexed MCP tool definitions.
|
|
20
|
-
* Returns
|
|
44
|
+
* Returns compact cards by default (signature + CLI hint). Full JSON schema is optional.
|
|
21
45
|
*/
|
|
22
46
|
export function createMcpSearchTool(deps: SearchDeps) {
|
|
23
47
|
return {
|
|
@@ -25,7 +49,7 @@ export function createMcpSearchTool(deps: SearchDeps) {
|
|
|
25
49
|
label: "MCP Search",
|
|
26
50
|
description:
|
|
27
51
|
"Search for MCP tools by describing what you want to do. " +
|
|
28
|
-
"Returns matching tool definitions with
|
|
52
|
+
"Returns matching tool definitions with compact signatures by default (schema optional). " +
|
|
29
53
|
`Use this before ${TOOL_MCP_CALL} to find the right tool name.`,
|
|
30
54
|
parameters: Type.Object({
|
|
31
55
|
query: Type.String({
|
|
@@ -36,6 +60,11 @@ export function createMcpSearchTool(deps: SearchDeps) {
|
|
|
36
60
|
description: "Max tools to return (default 5, max 20).",
|
|
37
61
|
}),
|
|
38
62
|
),
|
|
63
|
+
include_schema: Type.Optional(
|
|
64
|
+
Type.Boolean({
|
|
65
|
+
description: "Include full JSON parameter schema in results. Default is auto (enabled when mcporter is unavailable).",
|
|
66
|
+
}),
|
|
67
|
+
),
|
|
39
68
|
}),
|
|
40
69
|
|
|
41
70
|
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
|
@@ -49,6 +78,10 @@ export function createMcpSearchTool(deps: SearchDeps) {
|
|
|
49
78
|
|
|
50
79
|
const rawLimit = typeof params.limit === "number" ? params.limit : deps.cfg.topK;
|
|
51
80
|
const limit = Math.max(1, Math.min(20, rawLimit));
|
|
81
|
+
const includeSchema =
|
|
82
|
+
readBoolParam(params, "include_schema") ??
|
|
83
|
+
deps.cfg.includeParametersDefault ??
|
|
84
|
+
!deps.hasMcporter;
|
|
52
85
|
|
|
53
86
|
let vector: number[];
|
|
54
87
|
try {
|
|
@@ -101,25 +134,37 @@ export function createMcpSearchTool(deps: SearchDeps) {
|
|
|
101
134
|
const cards = results
|
|
102
135
|
.map((r, i) => {
|
|
103
136
|
const scoreStr = `${(r.score * 100).toFixed(0)}%`;
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
137
|
+
const signature = buildSignature(r.entry.tool_name, r.entry.parameters_json);
|
|
138
|
+
const cliHint = `mcporter call ${r.entry.server_name}.${r.entry.tool_name} '{...}'`;
|
|
139
|
+
const fallbackHint = `${TOOL_MCP_CALL}(\"${r.entry.tool_name}\", '{...}')`;
|
|
140
|
+
|
|
141
|
+
const schemaBlock = includeSchema
|
|
142
|
+
? `\n**Parameters (JSON Schema):**\n\`\`\`json\n${
|
|
143
|
+
r.entry.parameters_json.length > 2000
|
|
144
|
+
? r.entry.parameters_json.slice(0, 2000) + "\n... (truncated)"
|
|
145
|
+
: r.entry.parameters_json
|
|
146
|
+
}\n\`\`\``
|
|
147
|
+
: "";
|
|
109
148
|
|
|
110
149
|
return (
|
|
111
150
|
`### ${i + 1}. ${r.entry.tool_name} (server: ${r.entry.server_name}, score: ${scoreStr})\n` +
|
|
112
151
|
`**Description:** ${r.entry.description}\n` +
|
|
113
|
-
`**
|
|
152
|
+
`**Signature:** \`${signature}\`\n` +
|
|
153
|
+
`**Preferred (CLI):** \`${cliHint}\`\n` +
|
|
154
|
+
`**Fallback (JSON):** \`${fallbackHint}\`` +
|
|
155
|
+
schemaBlock
|
|
114
156
|
);
|
|
115
157
|
})
|
|
116
158
|
.join("\n\n");
|
|
117
159
|
|
|
118
|
-
const text =
|
|
160
|
+
const text =
|
|
161
|
+
`Found ${results.length} matching tool(s). ` +
|
|
162
|
+
`${includeSchema ? "Including full schema." : "Using compact mode (set include_schema=true for full JSON schema; schema auto-enables when mcporter is unavailable)."}\n\n` +
|
|
163
|
+
cards;
|
|
119
164
|
|
|
120
165
|
return {
|
|
121
166
|
content: [{ type: "text", text }],
|
|
122
|
-
details: { count: results.length },
|
|
167
|
+
details: { count: results.length, includeSchema },
|
|
123
168
|
};
|
|
124
169
|
},
|
|
125
170
|
};
|