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.
@@ -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
- **OpenClaw MCP Router** is a dynamic tool discovery layer for [OpenClaw](https://openclaw.ai). It uses semantic vector search to eliminate **Context Bloat** by routing only the necessary Model Context Protocol (MCP) tool schemas to your agent on-demand.
3
+ OpenClaw MCP Router is an OpenClaw plugin that keeps MCP tool catalogs out of the system prompt until needed.
4
4
 
5
- ## โšก The Problem: Context Window Exhaustion
5
+ Instead of injecting every MCP schema up front, it provides two lightweight meta-tools:
6
6
 
7
- Modern MCP catalogs are growing. Loading every tool schema upfront is expensive and inefficient:
7
+ - `mcp_search` โ†’ discover the right tool at runtime
8
+ - `mcp_call` โ†’ execute as JSON fallback
8
9
 
9
- * **Token Waste:** 5 MCP servers with 50+ tools can burn **55kโ€“134k tokens** before your agent even says "Hello."
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
- ## ๐Ÿ› ๏ธ The Solution: Semantic Tool Routing
14
-
15
- Instead of a full schema dump, this plugin registers two lightweight "Meta-Tools":
12
+ ---
16
13
 
17
- 1. **`mcp_search(query)`**: Uses **Ollama** and **LanceDB** to perform a semantic search. It returns only the top-N most relevant tool definitions (reducing overhead by ~95%).
18
- 2. **`mcp_call(tool_name, params)`**: Dynamically resolves the owning MCP server and executes the call.
14
+ ## Why this exists
19
15
 
20
- > **Result:** Your agent "asks" for the tools it needs, keeping the context window clean and the reasoning sharp.
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
- ## ๐Ÿš€ Quick Start
22
+ MCP Router applies Anthropic's tool-search pattern so only relevant tools are surfaced when needed.
25
23
 
26
- ### 1. Prerequisites
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
- Ensure you have **Ollama** running locally with an embedding model:
28
+ ---
29
29
 
30
- ```bash
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. Installation
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
- ```bash
38
- openclaw plugins install openclaw-mcp-router
47
+ ### 3) Execute (`mcp_call`)
48
+ - JSON-based execution path (classic MCP params flow)
39
49
 
40
- ```
50
+ ---
41
51
 
42
- ### 3. Setup & Indexing
52
+ ## CLI-first behavior (new)
43
53
 
44
- Run the interactive wizard to configure your servers and automatically update your `alsoAllow` permissions:
54
+ Router is now optimized for a CLI-first workflow:
45
55
 
46
- ```bash
47
- openclaw openclaw-mcp-router setup
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
- ## โš™๏ธ Configuration
63
+ ## Quick start
55
64
 
56
- The plugin is highly configurable via `~/.openclaw/openclaw.json`.
65
+ ### Prerequisites
57
66
 
58
- ### Server Management
67
+ ```bash
68
+ ollama pull embeddinggemma
69
+ ```
59
70
 
60
- You can manage servers via the **Interactive TUI**:
71
+ ### Install
61
72
 
62
73
  ```bash
63
- openclaw openclaw-mcp-router control
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
- ### Manual Schema Example
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
- For power users, add servers directly to your `plugins.entries`:
88
+ ## Key configuration
70
89
 
71
- | Key | Description | Default |
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
- "plugins": {
81
- "entries": {
82
- "openclaw-mcp-router": {
83
- "enabled": true,
84
- "config": {
85
- "servers": [{ "name": "filesystem", "transport": "stdio", "command": "npx", "args": ["..."] }],
86
- "embedding": { "provider": "ollama", "model": "embeddinggemma" }
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
- ## ๐Ÿง  How It Works: Under the Hood
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
- 1. **Indexing:** During `reindex`, the router connects to all configured MCP servers, fetches their manifests, and generates vector embeddings for every tool description.
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
- ## ๐Ÿ“ˆ Performance & Benchmarks
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
- Based on the [Anthropic Tool Search](https://www.anthropic.com/engineering/advanced-tool-use) pattern, dynamic routing can improve tool selection accuracy significantly:
139
+ ## Documentation
113
140
 
114
- * **Standard Loading:** ~49% Accuracy (Large catalogs)
115
- * **Dynamic Routing:** **~88% Accuracy** (Opus 4.5 benchmarks)
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
- ## ๐Ÿค Contributing
147
+ ## Contributing
120
148
 
121
- We are looking to implement **Hybrid Search (BM25)** and **LLM-based Reranking**. If you're interested in improving LLM orchestration efficiency, we'd love your help!
149
+ PRs welcome โ€” especially around:
150
+ - better reranking
151
+ - hybrid retrieval (vector + lexical)
122
152
 
123
- 1. Fork the repo.
124
- 2. Create your feature branch.
125
- 3. Submit a PR.
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
- ## ๐Ÿ“„ License
158
+ ## License
128
159
 
129
- Released under the [MIT License](https://www.google.com/search?q=LICENSE).
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>
@@ -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โ†’8.7k tokens by dynamically surfacing only relevant tools.",
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": { "type": "string" },
16
- "args": { "type": "array", "items": { "type": "string" } },
17
- "env": { "type": "object", "additionalProperties": { "type": "string" } },
18
- "url": { "type": "string" },
19
- "serverUrl": { "type": "string" },
20
- "type": { "type": "string", "enum": ["stdio", "sse", "http"] },
21
- "headers": { "type": "object", "additionalProperties": { "type": "string" } },
22
- "timeout": { "type": "number", "description": "Per-server connect timeout in ms; overrides indexer.connectTimeout" }
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": ["name", "transport"],
66
+ "required": [
67
+ "name",
68
+ "transport"
69
+ ],
36
70
  "properties": {
37
- "name": { "type": "string" },
38
- "transport": { "type": "string", "enum": ["stdio", "sse", "http"] },
39
- "command": { "type": "string" },
40
- "args": { "type": "array", "items": { "type": "string" } },
41
- "env": { "type": "object", "additionalProperties": { "type": "string" } },
42
- "url": { "type": "string" },
43
- "headers": { "type": "object", "additionalProperties": { "type": "string" } },
44
- "timeout": { "type": "number", "description": "Per-server connect timeout in ms; overrides indexer.connectTimeout" }
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": { "type": "number", "description": "Per-server default connect timeout in ms (default: 60000)" },
53
- "maxRetries": { "type": "number", "minimum": 0, "description": "Retry attempts per server, 0 = no retry (default: 3)" },
54
- "initialRetryDelay": { "type": "number", "description": "Initial backoff delay in ms (default: 2000)" },
55
- "maxRetryDelay": { "type": "number", "description": "Max backoff cap in ms (default: 30000)" },
56
- "maxChunkChars": { "type": "number", "minimum": 0, "description": "Max chars per chunk for long tool descriptions. 0 = disable chunking (default: 500)" },
57
- "overlapChars": { "type": "number", "minimum": 0, "description": "Overlap chars between adjacent chunks (default: 100)" }
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": { "type": "string", "enum": ["openai", "gemini", "voyage", "mistral", "ollama"] },
64
- "model": { "type": "string" },
65
- "baseUrl": { "type": "string" },
66
- "url": { "type": "string", "description": "Deprecated: use baseUrl. Old Ollama URL (gets /v1 appended)." },
67
- "apiKey": { "type": "string" },
68
- "headers": { "type": "object", "additionalProperties": { "type": "string" } }
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": { "path": { "type": "string" } }
186
+ "properties": {
187
+ "path": {
188
+ "type": "string"
189
+ }
190
+ }
74
191
  },
75
192
  "search": {
76
193
  "type": "object",
77
194
  "properties": {
78
- "topK": { "type": "number", "minimum": 1, "maximum": 20 },
79
- "minScore": { "type": "number", "minimum": 0, "maximum": 1 }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-mcp-router",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "description": "Dynamic MCP tool router for OpenClaw โ€” semantic search over large MCP catalogs to eliminate context bloat",
6
6
  "type": "module",
@@ -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. **Read schema**
25
- - Identify required fields, types, enums, nested structure.
26
- 4. **Call tool**
27
- - `mcp_call("exact_tool_name", "{...valid JSON...}")`
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
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: Write config
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
 
@@ -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
 
@@ -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 formatted tool cards with name, description, and parameter schema.
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 their parameter schemas. " +
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
- // Truncate large parameter schemas to keep context size bounded
105
- const paramsStr =
106
- r.entry.parameters_json.length > 2000
107
- ? r.entry.parameters_json.slice(0, 2000) + "\n... (truncated)"
108
- : r.entry.parameters_json;
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
- `**Parameters:**\n\`\`\`json\n${paramsStr}\n\`\`\``
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 = `Found ${results.length} matching tool(s):\n\n${cards}`;
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
  };
package/tsconfig.json CHANGED
@@ -3,7 +3,8 @@
3
3
  "target": "ESNext",
4
4
  "module": "ESNext",
5
5
  "moduleResolution": "bundler",
6
- "strict": true
6
+ "strict": true,
7
+ "skipLibCheck": true
7
8
  },
8
9
  "include": ["src/**/*"]
9
10
  }