@cryptotaxi247 / netdata-1 / commits / 891c583e6

Remote MCP support (streamable http and sse) (#21036)

* prepare for multiple mcp headends * fixed last * updated todo list * updated todo list * Add HTTP and SSE adapters for MCP * Select MCP SSE via Accept header * MCP: add HTTP/SSE transport support and fix chunked responses * MCP test client: await HTTP/SSE responses and harden SSE parsing * docs: drop completed MCP Phase 1/2 tasks from TODO * docs: renumber remaining MCP phases * docs: document HTTP/SSE MCP transports and remote-mcp usage * updated docs for mcp changes * bearer token in mcp-test-client * MCP authorization via http header

Costa Tsaousis committed Oct 2, 2025 at 13:41 UTC 891c583e6be2e40f207cdf48687b68d22c8207b2
52 files changed +4505 -1245
CMakeLists.txt
+9 -4
@@ -1814,6 +1814,13 @@ set(WEB_PLUGIN_FILES
1814 src/web/api/v3/api_v3_stream_path.c
1815 src/web/mcp/adapters/mcp-websocket.c
1816 src/web/mcp/adapters/mcp-websocket.h
1817 + src/web/mcp/adapters/mcp-http.c
1818 + src/web/mcp/adapters/mcp-http.h
1819 + src/web/mcp/adapters/mcp-http-common.h
1820 + src/web/mcp/adapters/mcp-sse.c
1821 + src/web/mcp/adapters/mcp-sse.h
1822 + src/web/mcp/mcp-jsonrpc.c
1823 + src/web/mcp/mcp-jsonrpc.h
1824 src/web/mcp/mcp-initialize.c
1825 src/web/mcp/mcp-initialize.h
1826 src/web/mcp/mcp-prompts.c
@@ -1839,16 +1846,14 @@ set(WEB_PLUGIN_FILES
1846 src/web/mcp/mcp-tools-configured-alerts.h
1847 src/web/mcp/mcp-params.c
1848 src/web/mcp/mcp-params.h
1842 - src/web/mcp/mcp-request-id.c
1843 - src/web/mcp/mcp-request-id.h
1849 src/web/mcp/mcp-ping.c
1850 src/web/mcp/mcp-ping.h
1851 src/web/mcp/mcp-logging.c
1852 src/web/mcp/mcp-logging.h
1853 src/web/mcp/mcp-completion.c
1854 src/web/mcp/mcp-completion.h
1850 - src/web/mcp/mcp-api-key.c
1851 - src/web/mcp/mcp-api-key.h
1855 + src/web/api/mcp_auth.c
1856 + src/web/api/mcp_auth.h
1857 src/web/mcp/mcp.c
1858 src/web/mcp/mcp.h
1859 src/web/server/static/static-threaded.c
docs/.map/map.csv
+4
@@ -190,6 +190,10 @@ https://github.com/netdata/netdata/edit/master/docs/ml-ai/ai-chat-netdata/jetbra
190 https://github.com/netdata/netdata/edit/master/docs/ml-ai/ai-chat-netdata/netdata-web-client.md,Netdata Web Client,Published,Netdata AI/MCP/MCP Clients,
191 https://github.com/netdata/netdata/edit/master/docs/ml-ai/ai-devops-copilot/claude-code.md,Claude Code,Published,Netdata AI/MCP/MCP Clients,
192 https://github.com/netdata/netdata/edit/master/docs/ml-ai/ai-devops-copilot/gemini-cli.md,Gemini CLI,Published,Netdata AI/MCP/MCP Clients,
193 +https://github.com/netdata/netdata/edit/master/docs/ml-ai/ai-devops-copilot/codex-cli.md,OpenAI Codex CLI,Published,Netdata AI/MCP/MCP Clients,
194 +https://github.com/netdata/netdata/edit/master/docs/ml-ai/ai-devops-copilot/crush.md,Crush,Published,Netdata AI/MCP/MCP Clients,
195 +https://github.com/netdata/netdata/edit/master/docs/ml-ai/ai-devops-copilot/opencode.md,OpenCode,Published,Netdata AI/MCP/MCP Clients,
196 +,,,
197 https://github.com/netdata/netdata/edit/master/docs/netdata-assistant.md,AI powered troubleshooting assistant,Unpublished,AI and Machine Learning,
198 https://github.com/netdata/netdata/edit/master/src/ml/README.md,ML models and anomaly detection,Unpublished,AI and Machine Learning,This is an in-depth look at how Netdata uses ML to detect anomalies.
199 ,,,,
docs/learn/mcp.md
+125 -7
@@ -4,10 +4,46 @@ All Netdata Agents and Parents are Model Context Protocol (MCP) servers, enablin
4
5 Every Netdata Agent and Parent includes an MCP server that:
6
7 -- Implements the protocol as WebSocket for transport
7 +- Implements the protocol with multiple transport options: WebSocket, HTTP streamable, and SSE (Server-Sent Events)
8 - Provides read-only access to metrics, logs, alerts, and live system information
9 - Requires no additional installation - it's part of Netdata
10
11 +## Transport Options
12 +
13 +Netdata MCP supports three transport mechanisms:
14 +
15 +| Transport | Endpoint | Use Case |
16 +|-----------|----------|----------|
17 +| **WebSocket** | `ws://YOUR_IP:19999/mcp` | Original transport, requires nd-mcp bridge for stdio clients |
18 +| **HTTP Streamable** | `http://YOUR_IP:19999/mcp` | Direct connection from AI clients supporting HTTP |
19 +| **SSE** | `http://YOUR_IP:19999/mcp?transport=sse` | Server-Sent Events for real-time streaming |
20 +
21 +### Direct Connection vs Bridge
22 +
23 +With the new HTTP and SSE transports, many AI clients can now connect directly to Netdata without needing the nd-mcp bridge:
24 +
25 +- **Direct Connection**: AI clients that support HTTP or SSE transports can connect directly to Netdata
26 +- **Bridge Required**: AI clients that only support stdio (like some desktop apps) still need the nd-mcp bridge or the official MCP remote client
27 +
28 +### Official MCP Remote Client
29 +
30 +If your AI client doesn't support HTTP/SSE directly and you don't want to use nd-mcp, you can use the official MCP remote client:
31 +
32 +```bash
33 +# Export your MCP key once per shell
34 +export NETDATA_MCP_API_KEY="$(cat /var/lib/netdata/mcp_dev_preview_api_key)"
35 +
36 +# For SSE transport
37 +npx mcp-remote@latest --sse http://YOUR_NETDATA_IP:19999/mcp \
38 + --allow-http \
39 + --header "Authorization: Bearer $NETDATA_MCP_API_KEY"
40 +
41 +# For HTTP transport
42 +npx mcp-remote@latest --http http://YOUR_NETDATA_IP:19999/mcp \
43 + --allow-http \
44 + --header "Authorization: Bearer $NETDATA_MCP_API_KEY"
45 +```
46 +
47 ## Visibility Scope
48
49 Netdata provides comprehensive access to all available observability data through MCP, including complete metadata:
@@ -22,7 +58,7 @@ Netdata provides comprehensive access to all available observability data throug
58 - **Function Execution** - Execute Netdata functions on any connected node (requires Netdata Parent)
59 - **Log Exploration** - Access logs from any connected node (requires Netdata Parent)
60
25 -For sensitive features currently protected by Netdata Cloud SSO, a temporary MCP API key is generated on each Netdata instance. When included in the MCP connection string, this key unlocks access to sensitive data and protected functions (like `systemd-journal`, `windows-events` and `processes`). This temporary API key mechanism will eventually be replaced with a new authentication system integrated with Netdata Cloud.
61 +For sensitive features currently protected by Netdata Cloud SSO, a temporary MCP API key is generated on each Netdata instance. When presented via the `Authorization: Bearer` header, this key unlocks access to sensitive data and protected functions (like `systemd-journal`, `windows-events` and `processes`). This temporary API key mechanism will eventually be replaced with a new authentication system integrated with Netdata Cloud.
62
63 AI assistants have different visibility depending on where they connect:
64
@@ -32,6 +68,13 @@ AI assistants have different visibility depending on where they connect:
68
69 ## Finding the nd-mcp Bridge
70
71 +> **Note**: With the new HTTP and SSE transports, many AI clients can now connect directly to Netdata without nd-mcp. Check your AI client's documentation to see if it supports direct HTTP or SSE connections.
72 +
73 +The nd-mcp bridge is only needed for AI clients that:
74 +- Only support `stdio` communication (like some desktop applications)
75 +- Cannot use HTTP or SSE transports directly
76 +- Cannot use `npx mcp-remote@latest`
77 +
78 AI clients like Claude Desktop run locally on your computer and use `stdio` communication. Since your Netdata runs remotely on a server, you need a bridge to convert `stdio` to WebSocket communication.
79
80 The `nd-mcp` bridge needs to be available on your desktop or laptop where your AI client runs. Since most users run Netdata on remote servers rather than their local machines, you have two options:
@@ -201,7 +244,45 @@ If the file doesn't exist:
244
245 ## AI Client Configuration
246
204 -Most AI clients use a similar configuration format:
247 +AI clients can connect to Netdata MCP in different ways depending on their transport support:
248 +
249 +### Direct Connection (HTTP/SSE)
250 +
251 +For AI clients that support HTTP or SSE transports:
252 +
253 +```json
254 +{
255 + "mcpServers": {
256 + "netdata": {
257 + "type": "http",
258 + "url": "http://IP_OF_YOUR_NETDATA:19999/mcp",
259 + "headers": [
260 + "Authorization: Bearer YOUR_API_KEY"
261 + ]
262 + }
263 + }
264 +}
265 +```
266 +
267 +Or for SSE:
268 +
269 +```json
270 +{
271 + "mcpServers": {
272 + "netdata": {
273 + "type": "sse",
274 + "url": "http://IP_OF_YOUR_NETDATA:19999/mcp?transport=sse",
275 + "headers": [
276 + "Authorization: Bearer YOUR_API_KEY"
277 + ]
278 + }
279 + }
280 +}
281 +```
282 +
283 +### Using nd-mcp Bridge (stdio)
284 +
285 +For AI clients that only support stdio:
286
287 ```json
288 {
@@ -209,7 +290,28 @@ Most AI clients use a similar configuration format:
290 "netdata": {
291 "command": "/usr/sbin/nd-mcp",
292 "args": [
212 - "ws://IP_OF_YOUR_NETDATA:19999/mcp?api_key=YOUR_API_KEY"
293 + "--bearer",
294 + "YOUR_API_KEY",
295 + "ws://IP_OF_YOUR_NETDATA:19999/mcp"
296 + ]
297 + }
298 + }
299 +}
300 +```
301 +
302 +### Using Official MCP Remote Client
303 +
304 +```json
305 +{
306 + "mcpServers": {
307 + "netdata": {
308 + "command": "npx",
309 + "args": [
310 + "mcp-remote@latest",
311 + "--http",
312 + "http://IP_OF_YOUR_NETDATA:19999/mcp",
313 + "--header",
314 + "Authorization: Bearer YOUR_API_KEY"
315 ]
316 }
317 }
@@ -218,9 +320,9 @@ Most AI clients use a similar configuration format:
320
321 Replace:
322
221 -- `/usr/sbin/nd-mcp` - With your actual nd-mcp path
323 - `IP_OF_YOUR_NETDATA`: Your Netdata instance IP/hostname
324 - `YOUR_API_KEY`: The API key from the file mentioned above
325 +- `/usr/sbin/nd-mcp`: With your actual nd-mcp path (if using the bridge)
326
327 ### Multiple MCP Servers
328
@@ -231,14 +333,30 @@ You can configure multiple Netdata instances:
333 "mcpServers": {
334 "netdata-production": {
335 "command": "/usr/sbin/nd-mcp",
234 - "args": ["ws://prod-parent:19999/mcp?api_key=PROD_KEY"]
336 + "args": ["--bearer", "PROD_KEY", "ws://prod-parent:19999/mcp"]
337 },
338 "netdata-testing": {
339 "command": "/usr/sbin/nd-mcp",
238 - "args": ["ws://test-parent:19999/mcp?api_key=TEST_KEY"]
340 + "args": ["--bearer", "TEST_KEY", "ws://test-parent:19999/mcp"]
341 }
342 }
343 }
344 ```
345
346 Note: Most AI clients have difficulty choosing between multiple MCP servers. You may need to enable/disable them manually.
347 +
348 +### Legacy Query String Support
349 +
350 +For compatibility with older tooling, Netdata still accepts the `?api_key=YOUR_API_KEY` query parameter on the `/mcp` endpoints. New integrations should prefer the `Authorization: Bearer YOUR_API_KEY` header, but the query-string form remains available if you are migrating gradually.
351 +
352 +## AI Client Specific Documentation
353 +
354 +For detailed configuration instructions for specific AI clients, see:
355 +
356 +- [Claude Code](/docs/ml-ai/ai-devops-copilot/claude-code.md) - Anthropic's CLI for Claude
357 +- [Gemini CLI](/docs/ml-ai/ai-devops-copilot/gemini-cli.md) - Google's Gemini CLI
358 +- [OpenAI Codex CLI](/docs/ml-ai/ai-devops-copilot/codex-cli.md) - OpenAI's Codex CLI
359 +- [Crush](/docs/ml-ai/ai-devops-copilot/crush.md) - Charmbracelet's glamorous terminal AI
360 +- [OpenCode](/docs/ml-ai/ai-devops-copilot/opencode.md) - SST's terminal-based AI assistant
361 +
362 +Each guide includes specific transport support matrices and configuration examples optimized for that client.
docs/ml-ai/ai-chat-netdata/claude-desktop.md
+11 -5
@@ -7,7 +7,11 @@ Configure Claude Desktop to access your Netdata infrastructure through MCP.
7 1. **Claude Desktop installed** - Download from [claude.ai/download](https://claude.ai/download)
8 2. **The IP and port (usually 19999) of a running Netdata Agent** - Prefer a Netdata Parent to get infrastructure level visibility. Currently the latest nightly version of Netdata has MCP support (not released to the stable channel yet). Your AI Client (running on your desktop or laptop) needs to have direct network access to this IP and port.
9 3. **`nd-mcp` program available on your desktop or laptop** - This is the bridge that translates `stdio` to `websocket`, connecting your AI Client to your Netdata Agent or Parent. [Find its absolute path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge)
10 -4. **Optionally, the Netdata MCP API key** that unlocks full access to sensitive observability data (protected functions, full access to logs) on your Netdata. Each Netdata Agent or Parent has its own unique API key for MCP - [Find your Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key)
10 +4. **Netdata MCP API key loaded into the environment** (recommended) - export it before launching Claude Code to avoid exposing it in config files:
11 + ```bash
12 + export ND_MCP_BEARER_TOKEN="$(cat /var/lib/netdata/mcp_dev_preview_api_key)"
13 + ```
14 + Each Netdata Agent or Parent has its own unique API key for MCP - [Find your Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key)
15
16 ## Platform-Specific Installation
17
@@ -37,7 +41,7 @@ Use the community AppImage project:
41 "netdata": {
42 "command": "/usr/sbin/nd-mcp",
43 "args": [
40 - "ws://YOUR_NETDATA_IP:19999/mcp?api_key=NETDATA_MCP_API_KEY"
44 + "ws://YOUR_NETDATA_IP:19999/mcp"
45 ]
46 }
47 }
@@ -48,7 +52,7 @@ Replace:
52
53 - `/usr/sbin/nd-mcp` - With your [actual nd-mcp path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge)
54 - `YOUR_NETDATA_IP` - IP address or hostname of your Netdata Agent/Parent
51 -- `NETDATA_MCP_API_KEY` - Your [Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key)
55 +- `ND_MCP_BEARER_TOKEN` - Export this environment variable with your [Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key) before launching Claude Desktop
56
57 5. Save the configuration
58 6. **Restart Claude Desktop** (required for changes to take effect)
@@ -84,11 +88,11 @@ Add multiple configurations and enable/disable as needed:
88 "mcpServers": {
89 "netdata-production": {
90 "command": "/usr/sbin/nd-mcp",
87 - "args": ["ws://prod-parent:19999/mcp?api_key=PROD_KEY"]
91 + "args": ["ws://prod-parent:19999/mcp"]
92 },
93 "netdata-staging": {
94 "command": "/usr/sbin/nd-mcp",
91 - "args": ["ws://stage-parent:19999/mcp?api_key=STAGE_KEY"]
95 + "args": ["ws://stage-parent:19999/mcp"]
96 }
97 }
98 }
@@ -96,6 +100,8 @@ Add multiple configurations and enable/disable as needed:
100
101 Use the toggle switch in settings to enable only one at a time.
102
103 +> ℹ️ Set `ND_MCP_BEARER_TOKEN` to the appropriate key before switching between environments to avoid storing secrets in the configuration file.
104 +
105 ### Option 2: Single Parent
106
107 Connect to your main Netdata Parent that has visibility across all environments.
docs/ml-ai/ai-chat-netdata/cursor.md
+10 -4
@@ -7,7 +7,11 @@ Configure Cursor IDE to access your Netdata infrastructure through MCP.
7 1. **Cursor installed** - Download from [cursor.com](https://www.cursor.com)
8 2. **The IP and port (usually 19999) of a running Netdata Agent** - Prefer a Netdata Parent to get infrastructure level visibility. Currently the latest nightly version of Netdata has MCP support (not released to the stable channel yet). Your AI Client (running on your desktop or laptop) needs to have direct network access to this IP and port.
9 3. **`nd-mcp` program available on your desktop or laptop** - This is the bridge that translates `stdio` to `websocket`, connecting your AI Client to your Netdata Agent or Parent. [Find its absolute path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge)
10 -4. **Optionally, the Netdata MCP API key** that unlocks full access to sensitive observability data (protected functions, full access to logs) on your Netdata. Each Netdata Agent or Parent has its own unique API key for MCP - [Find your Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key)
10 +4. **Netdata MCP API key loaded into the environment** (recommended) - export it before launching Cursor:
11 + ```bash
12 + export ND_MCP_BEARER_TOKEN="$(cat /var/lib/netdata/mcp_dev_preview_api_key)"
13 + ```
14 + Each Netdata Agent or Parent has its own unique API key for MCP - [Find your Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key)
15
16 ## Configuration
17
@@ -26,7 +30,7 @@ The configuration format:
30 "netdata": {
31 "command": "/usr/sbin/nd-mcp",
32 "args": [
29 - "ws://YOUR_NETDATA_IP:19999/mcp?api_key=NETDATA_MCP_API_KEY"
33 + "ws://YOUR_NETDATA_IP:19999/mcp"
34 ]
35 }
36 }
@@ -80,11 +84,11 @@ Cursor allows multiple MCP servers but requires manual toggling:
84 "mcpServers": {
85 "netdata-prod": {
86 "command": "/usr/sbin/nd-mcp",
83 - "args": ["ws://prod-parent:19999/mcp?api_key=PROD_KEY"]
87 + "args": ["ws://prod-parent:19999/mcp"]
88 },
89 "netdata-dev": {
90 "command": "/usr/sbin/nd-mcp",
87 - "args": ["ws://dev-parent:19999/mcp?api_key=DEV_KEY"]
91 + "args": ["ws://dev-parent:19999/mcp"]
92 }
93 }
94 }
@@ -92,6 +96,8 @@ Cursor allows multiple MCP servers but requires manual toggling:
96
97 Use the toggle in settings to enable only the environment you need.
98
99 +> ℹ️ Before switching environments, set `ND_MCP_BEARER_TOKEN` to the matching key so the bridge picks up the correct credentials without embedding them in the config file.
100 +
101 ## Best Practices
102
103 ### Infrastructure-Aware Development
docs/ml-ai/ai-chat-netdata/jetbrains-ides.md
+8 -4
@@ -20,7 +20,11 @@ Configure JetBrains IDEs to access your Netdata infrastructure through MCP.
20 2. **AI Assistant plugin** - Install from IDE marketplace
21 3. **The IP and port (usually 19999) of a running Netdata Agent** - Prefer a Netdata Parent to get infrastructure level visibility. Currently the latest nightly version of Netdata has MCP support (not released to the stable channel yet). Your AI Client (running on your desktop or laptop) needs to have direct network access to this IP and port.
22 4. **`nd-mcp` program available on your desktop or laptop** - This is the bridge that translates `stdio` to `websocket`, connecting your AI Client to your Netdata Agent or Parent. [Find its absolute path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge)
23 -5. **Optionally, the Netdata MCP API key** that unlocks full access to sensitive observability data (protected functions, full access to logs) on your Netdata. Each Netdata Agent or Parent has its own unique API key for MCP - [Find your Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key)
23 +5. **Netdata MCP API key exported before launching the IDE**:
24 + ```bash
25 + export ND_MCP_BEARER_TOKEN="$(cat /var/lib/netdata/mcp_dev_preview_api_key)"
26 + ```
27 + Each Netdata Agent or Parent has its own unique API key for MCP - [Find your Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key)
28
29 ## Installing AI Assistant
30
@@ -48,7 +52,7 @@ MCP support in JetBrains IDEs may require additional plugins or configuration. C
52 "name": "netdata",
53 "command": "/usr/sbin/nd-mcp",
54 "args": [
51 - "ws://YOUR_NETDATA_IP:19999/mcp?api_key=NETDATA_MCP_API_KEY"
55 + "ws://YOUR_NETDATA_IP:19999/mcp"
56 ]
57 }
58 ```
@@ -62,13 +66,13 @@ If direct MCP support is not available, configure as an External Tool:
66 3. Configure:
67 - **Name**: Netdata MCP
68 - **Program**: `/usr/sbin/nd-mcp`
65 - - **Arguments**: `ws://YOUR_NETDATA_IP:19999/mcp?api_key=NETDATA_MCP_API_KEY`
69 + - **Arguments**: `ws://YOUR_NETDATA_IP:19999/mcp`
70
71 Replace:
72
73 - `/usr/sbin/nd-mcp` - With your [actual nd-mcp path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge)
74 - `YOUR_NETDATA_IP` - IP address or hostname of your Netdata Agent/Parent
71 -- `NETDATA_MCP_API_KEY` - Your [Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key)
75 +- `ND_MCP_BEARER_TOKEN` - Export with your [Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key) before launching the IDE
76
77 ## Usage in Different IDEs
78
docs/ml-ai/ai-chat-netdata/vs-code.md
+11 -5
@@ -18,7 +18,11 @@ Autonomous coding agent that can use MCP tools.
18 2. **MCP-compatible extension** - Install from VS Code Marketplace
19 3. **The IP and port (usually 19999) of a running Netdata Agent** - Prefer a Netdata Parent to get infrastructure level visibility. Currently the latest nightly version of Netdata has MCP support (not released to the stable channel yet). Your AI Client (running on your desktop or laptop) needs to have direct network access to this IP and port.
20 4. **`nd-mcp` program available on your desktop or laptop** - This is the bridge that translates `stdio` to `websocket`, connecting your AI Client to your Netdata Agent or Parent. [Find its absolute path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge)
21 -5. **Optionally, the Netdata MCP API key** that unlocks full access to sensitive observability data (protected functions, full access to logs) on your Netdata. Each Netdata Agent or Parent has its own unique API key for MCP - [Find your Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key)
21 +5. **Netdata MCP API key exported before launching VS Code** - keep secrets out of config files by setting:
22 + ```bash
23 + export ND_MCP_BEARER_TOKEN="$(cat /var/lib/netdata/mcp_dev_preview_api_key)"
24 + ```
25 + Each Netdata Agent or Parent has its own unique API key for MCP - [Find your Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key)
26
27 ## Continue Extension Setup
28
@@ -56,13 +60,13 @@ Autonomous coding agent that can use MCP tools.
60 - name: netdata
61 command: /usr/sbin/nd-mcp
62 args:
59 - - ws://YOUR_NETDATA_IP:19999/mcp?api_key=NETDATA_MCP_API_KEY
63 + - ws://YOUR_NETDATA_IP:19999/mcp
64 env: {}
65 ```
66 5. Replace:
67 - `/usr/sbin/nd-mcp` with your actual nd-mcp path
68 - `YOUR_NETDATA_IP` with your Netdata instance IP/hostname
65 - - `NETDATA_MCP_API_KEY` with your Netdata MCP API key
69 + - `ND_MCP_BEARER_TOKEN` exported with your Netdata MCP API key before launching VS Code
70 6. Save the file
71
72 ### Usage
@@ -95,7 +99,7 @@ Press `Ctrl+L` to open Continue chat, then:
99 "name": "netdata",
100 "command": "/usr/sbin/nd-mcp",
101 "args": [
98 - "ws://YOUR_NETDATA_IP:19999/mcp?api_key=NETDATA_MCP_API_KEY"
102 + "ws://YOUR_NETDATA_IP:19999/mcp"
103 ]
104 }
105 ]
@@ -128,7 +132,7 @@ Create `.vscode/settings.json` in your project:
132 "netdata-prod": {
133 "command": "/usr/sbin/nd-mcp",
134 "args": [
131 - "ws://prod-parent:19999/mcp?api_key=PROD_NETDATA_MCP_API_KEY"
135 + "ws://prod-parent:19999/mcp"
136 ]
137 }
138 }
@@ -143,6 +147,8 @@ Different projects can have different Netdata connections:
147 - `~/projects/backend/.vscode/settings.json` → Backend servers
148 - `~/projects/infrastructure/.vscode/settings.json` → All servers
149
150 +> ℹ️ Export `ND_MCP_BEARER_TOKEN` with the appropriate key before opening VS Code so the bridge picks up credentials without storing them in `.vscode/settings.json`.
151 +
152 ## Advanced Usage
153
154 ### Custom Commands
docs/ml-ai/ai-devops-copilot/claude-code.md
+124 -19
@@ -2,40 +2,97 @@
2
3 Configure Claude Code to access your Netdata infrastructure through MCP.
4
5 +## Transport Support
6 +
7 +Claude Code supports multiple MCP transport types, giving you flexibility in how you connect to Netdata:
8 +
9 +| Transport | Support | Use Case |
10 +|-----------|---------|----------|
11 +| **stdio** (via nd-mcp bridge) | ✅ Fully Supported | Local bridge to WebSocket |
12 +| **Streamable HTTP** | ✅ Fully Supported | Direct connection to Netdata's HTTP endpoint |
13 +| **SSE** (Server-Sent Events) | ⚠️ Limited Support | Legacy, being deprecated |
14 +| **WebSocket** | ❌ Not Supported | Use nd-mcp bridge or HTTP instead |
15 +
16 ## Prerequisites
17
18 1. **Claude Code installed** - Available at [anthropic.com/claude-code](https://www.anthropic.com/claude-code)
19 2. **The IP and port (usually 19999) of a running Netdata Agent** - Prefer a Netdata Parent to get infrastructure level visibility. Currently the latest nightly version of Netdata has MCP support (not released to the stable channel yet). Your AI Client (running on your desktop or laptop) needs to have direct network access to this IP and port.
9 -3. **`nd-mcp` program available on your desktop or laptop** - This is the bridge that translates `stdio` to `websocket`, connecting your AI Client to your Netdata Agent or Parent. [Find its absolute path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge)
20 +3. **For stdio connections only: `nd-mcp` bridge** - The stdio-to-websocket bridge. [Find its absolute path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge). Not needed for direct HTTP connections.
21 4. **Optionally, the Netdata MCP API key** that unlocks full access to sensitive observability data (protected functions, full access to logs) on your Netdata. Each Netdata Agent or Parent has its own unique API key for MCP - [Find your Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key)
22
12 -## Configuration
23 +## Configuration Methods
24
25 Claude Code has comprehensive MCP server management capabilities. For detailed documentation on all configuration options and commands, see the [official Claude Code MCP documentation](https://docs.anthropic.com/en/docs/claude-code/mcp).
26
16 -### Adding Netdata MCP Server
27 +### Method 1: Direct HTTP Connection (Recommended)
28 +
29 +Connect directly to Netdata's HTTP endpoint without needing the nd-mcp bridge:
30 +
31 +```bash
32 +# Add Netdata via direct HTTP connection (project-scoped for team sharing)
33 +claude mcp add --transport http --scope project netdata \
34 + http://YOUR_NETDATA_IP:19999/mcp \
35 + --header "Authorization: Bearer NETDATA_MCP_API_KEY"
36 +
37 +# Or add locally for personal use only
38 +claude mcp add --transport http netdata \
39 + http://YOUR_NETDATA_IP:19999/mcp \
40 + --header "Authorization: Bearer NETDATA_MCP_API_KEY"
41 +
42 +# For HTTPS connections
43 +claude mcp add --transport http --scope project netdata \
44 + https://YOUR_NETDATA_IP:19999/mcp \
45 + --header "Authorization: Bearer NETDATA_MCP_API_KEY"
46 +```
47 +
48 +### Method 2: Using nd-mcp Bridge (stdio)
49
18 -Use Claude Code's built-in MCP commands to add your Netdata server:
50 +For environments where you prefer or need to use the bridge:
51
52 ```bash
21 -# Add Netdata MCP server (project-scoped for team sharing)
22 -claude mcp add --scope project netdata /usr/sbin/nd-mcp ws://YOUR_NETDATA_IP:19999/mcp?api_key=NETDATA_MCP_API_KEY
53 +# Add Netdata via nd-mcp bridge (project-scoped)
54 +claude mcp add --scope project netdata /usr/sbin/nd-mcp \
55 + --bearer NETDATA_MCP_API_KEY \
56 + ws://YOUR_NETDATA_IP:19999/mcp
57
58 # Or add locally for personal use only
25 -claude mcp add netdata /usr/sbin/nd-mcp ws://YOUR_NETDATA_IP:19999/mcp?api_key=NETDATA_MCP_API_KEY
59 +claude mcp add netdata /usr/sbin/nd-mcp \
60 + --bearer NETDATA_MCP_API_KEY \
61 + ws://YOUR_NETDATA_IP:19999/mcp
62 +```
63 +
64 +### Method 3: Using npx remote-mcp (Alternative Bridge)
65 +
66 +If nd-mcp is not available, you can use the official MCP remote client:
67 +
68 +```bash
69 +# Using SSE transport
70 +claude mcp add --scope project netdata npx mcp-remote@latest \
71 + --sse http://YOUR_NETDATA_IP:19999/mcp \
72 + --allow-http \
73 + --header "Authorization: Bearer NETDATA_MCP_API_KEY"
74 +
75 +# Using HTTP transport
76 +claude mcp add --scope project netdata npx mcp-remote@latest \
77 + --http http://YOUR_NETDATA_IP:19999/mcp \
78 + --allow-http \
79 + --header "Authorization: Bearer NETDATA_MCP_API_KEY"
80 +```
81 +
82 +### Verify Configuration
83
27 -# List configured servers to verify
84 +```bash
85 +# List configured servers
86 claude mcp list
87
88 # Get server details
89 claude mcp get netdata
90 ```
91
34 -Replace:
35 -
36 -- `/usr/sbin/nd-mcp` - With your [actual nd-mcp path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge)
92 +Replace in all examples:
93 - `YOUR_NETDATA_IP` - IP address or hostname of your Netdata Agent/Parent
94 - `NETDATA_MCP_API_KEY` - Your [Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key)
95 +- `/usr/sbin/nd-mcp` - With your [actual nd-mcp path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge) (stdio method only)
96
97 **Project-scoped configuration** creates a `.mcp.json` file that can be shared with your team via version control.
98
@@ -69,9 +126,11 @@ This is particularly useful when you have multiple MCP servers configured and wa
126
127 ## Project-Based Configuration
128
72 -Claude Code's strength is project-specific configurations. So you can have different project directories with different MCP servers on each of them, allowing you to control the MCP servers that will be used, based on the directory from which you started it.
129 +Claude Code's strength is project-specific configurations. You can have different project directories with different MCP servers, allowing you to control the MCP servers based on the directory from which you started Claude Code.
130 +
131 +### Configuration File Format (`.mcp.json`)
132
74 -### Production Environment
133 +#### Direct HTTP Connection (Recommended)
134
135 Create `~/projects/production/.mcp.json`:
136
@@ -79,31 +138,68 @@ Create `~/projects/production/.mcp.json`:
138 {
139 "mcpServers": {
140 "netdata": {
82 - "command": "/usr/sbin/nd-mcp",
83 - "args": ["ws://prod-parent.company.com:19999/mcp?api_key=PROD_KEY"]
141 + "type": "http",
142 + "url": "http://prod-parent.company.com:19999/mcp",
143 + "headers": [
144 + "Authorization: Bearer ${NETDATA_API_KEY}"
145 + ]
146 }
147 }
148 }
149 ```
150
89 -### Development Environment
151 +#### Using nd-mcp Bridge
152
91 -Create `~/projects/development/.mcp.json`:
153 +Create `~/projects/production/.mcp.json`:
154
155 ```json
156 {
157 "mcpServers": {
158 "netdata": {
159 "command": "/usr/sbin/nd-mcp",
98 - "args": ["ws://dev-parent.company.com:19999/mcp?api_key=DEV_KEY"]
160 + "args": [
161 + "--bearer",
162 + "${NETDATA_API_KEY}",
163 + "ws://prod-parent.company.com:19999/mcp"
164 + ]
165 }
166 }
167 }
168 ```
169
170 +#### Using npx remote-mcp
171 +
172 +Create `~/projects/production/.mcp.json`:
173 +
174 +```json
175 +{
176 + "mcpServers": {
177 + "netdata": {
178 + "command": "npx",
179 + "args": [
180 + "mcp-remote@latest",
181 + "--sse",
182 + "http://prod-parent.company.com:19999/mcp",
183 + "--allow-http",
184 + "--header",
185 + "Authorization: Bearer ${NETDATA_API_KEY}",
186 + ]
187 + }
188 + }
189 +}
190 +```
191 +
192 +### Environment Variables
193 +
194 +Claude Code supports environment variable expansion in `.mcp.json`:
195 +- `${VAR}` - Expands to the value of environment variable `VAR`
196 +- `${VAR:-default}` - Uses `VAR` if set, otherwise uses `default`
197 +
198 +This allows you to keep sensitive API keys out of version control.
199 +
200 ## Claude Instructions
201
106 -Create a `Claude.md` file in your project root with default instructions:
202 +Create a `CLAUDE.md` file in your project root with default instructions:
203
204 ```markdown
205 # Claude Instructions
@@ -140,3 +236,12 @@ Our key services to monitor:
236
237 - Verify API key is included in the connection string
238 - Check that the Netdata agent is claimed
239 +
240 +## Documentation Links
241 +
242 +- [Official Claude Code Documentation](https://docs.claude.com/en/docs/claude-code)
243 +- [Claude Code MCP Configuration Guide](https://docs.claude.com/en/docs/claude-code/mcp)
244 +- [Claude Code Getting Started](https://docs.claude.com/en/docs/claude-code/getting-started)
245 +- [Claude Code Commands Reference](https://docs.claude.com/en/docs/claude-code/commands)
246 +- [Netdata MCP Setup](/docs/learn/mcp.md)
247 +- [AI DevOps Best Practices](/docs/ml-ai/ai-devops-copilot/ai-devops-copilot.md)
docs/ml-ai/ai-devops-copilot/codex-cli.md new
+250
@@ -0,0 +1,250 @@
1 +# OpenAI Codex CLI
2 +
3 +Configure OpenAI's Codex CLI to access your Netdata infrastructure through MCP for AI-powered DevOps operations.
4 +
5 +## Transport Support
6 +
7 +Codex CLI currently has limited MCP transport support:
8 +
9 +| Transport | Support | Use Case |
10 +|-----------|---------|----------|
11 +| **stdio** (via nd-mcp bridge) | ✅ Supported | Local bridge to WebSocket |
12 +| **stdio** (via npx remote-mcp) | ✅ Supported | Alternative bridge with HTTP/SSE support |
13 +| **Streamable HTTP** | ❌ Not Supported | Use npx remote-mcp bridge |
14 +| **SSE** (Server-Sent Events) | ❌ Not Supported | Use npx remote-mcp bridge |
15 +| **WebSocket** | ❌ Not Supported | Use nd-mcp bridge |
16 +
17 +> **Note:** Codex CLI currently only supports stdio-based MCP servers. For HTTP/SSE connections to Netdata, you must use a bridge like nd-mcp or npx remote-mcp.
18 +
19 +## Prerequisites
20 +
21 +1. **OpenAI Codex CLI installed** - Available via npm, Homebrew, or direct download from [GitHub](https://github.com/openai/codex)
22 +2. **The IP and port (usually 19999) of a running Netdata Agent** - Prefer a Netdata Parent to get infrastructure level visibility. Currently the latest nightly version of Netdata has MCP support (not released to the stable channel yet). Your AI Client (running on your desktop or laptop) needs to have direct network access to this IP and port.
23 +3. **Bridge required: Choose one:**
24 + - `nd-mcp` bridge - The stdio-to-websocket bridge. [Find its absolute path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge)
25 + - `npx mcp-remote@latest` - Official MCP remote client supporting HTTP/SSE
26 +4. **Optionally, the Netdata MCP API key** that unlocks full access to sensitive observability data (protected functions, full access to logs) on your Netdata. Each Netdata Agent or Parent has its own unique API key for MCP - [Find your Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key)
27 +
28 +## Installation
29 +
30 +Install Codex CLI using one of these methods:
31 +
32 +```bash
33 +# Using npm (recommended)
34 +npm install -g @openai/codex
35 +
36 +# Using Homebrew (macOS)
37 +brew install codex
38 +
39 +# Or download directly from GitHub releases
40 +# https://github.com/openai/codex/releases
41 +```
42 +
43 +## Configuration Methods
44 +
45 +Codex CLI uses a TOML configuration file at `~/.codex/config.toml` for MCP server settings.
46 +
47 +### Method 1: Using npx remote-mcp (Recommended for HTTP/SSE)
48 +
49 +This method allows Codex CLI to connect to Netdata's HTTP/SSE endpoints through the official MCP remote client:
50 +
51 +```toml
52 +# ~/.codex/config.toml
53 +
54 +[mcp_servers.netdata]
55 +command = "npx"
56 +args = [
57 + "mcp-remote@latest",
58 + "--http",
59 + "--allow-http",
60 + "http://YOUR_NETDATA_IP:19999/mcp",
61 + "--header",
62 + "Authorization: Bearer NETDATA_MCP_API_KEY"
63 +]
64 +startup_timeout_sec = 20 # Optional: increase for remote connections
65 +tool_timeout_sec = 120 # Optional: increase for complex queries
66 +```
67 +
68 +For SSE transport instead of HTTP:
69 +
70 +```toml
71 +[mcp_servers.netdata]
72 +command = "npx"
73 +args = [
74 + "mcp-remote@latest",
75 + "--sse",
76 + "http://YOUR_NETDATA_IP:19999/mcp",
77 + "--allow-http",
78 + "--header",
79 + "Authorization: Bearer NETDATA_MCP_API_KEY",
80 +]
81 +```
82 +
83 +### Method 2: Using nd-mcp Bridge
84 +
85 +For environments where nd-mcp is available and preferred:
86 +
87 +```toml
88 +# ~/.codex/config.toml
89 +
90 +[mcp_servers.netdata]
91 +command = "/usr/sbin/nd-mcp"
92 +args = ["ws://YOUR_NETDATA_IP:19999/mcp"]
93 +env = { "ND_MCP_BEARER_TOKEN" = "YOUR_API_KEY_HERE" }
94 +startup_timeout_sec = 15
95 +tool_timeout_sec = 60
96 +
97 +[mcp_servers.netdata_prod]
98 +command = "/usr/sbin/nd-mcp"
99 +args = ["ws://prod-parent:19999/mcp"]
100 +env = { "ND_MCP_BEARER_TOKEN" = "${NETDATA_PROD_API_KEY}" }
101 +```
102 +
103 +Export `ND_MCP_BEARER_TOKEN` before starting Codex CLI (or define it in your shell profile) so the bridge authenticates without exposing the key in command-line arguments.
104 +
105 +When Codex CLI starts the bridge it will inject the environment variable, so `nd-mcp` authenticates without exposing the token in the connection arguments.
106 +
107 +## CLI Management (Experimental)
108 +
109 +Codex CLI provides experimental commands for managing MCP servers:
110 +
111 +```bash
112 +# Add a new MCP server
113 +codex mcp add netdata -- npx mcp-remote@latest --http http://YOUR_NETDATA_IP:19999/mcp \
114 + --allow-http \
115 + --header "Authorization: Bearer NETDATA_MCP_API_KEY"
116 +
117 +# List configured MCP servers
118 +codex mcp list
119 +
120 +# Remove an MCP server
121 +codex mcp remove netdata
122 +```
123 +
124 +## Verify Configuration
125 +
126 +After configuring, verify that Netdata MCP is available:
127 +
128 +1. Start Codex CLI:
129 + ```bash
130 + codex
131 + ```
132 +
133 +2. Check available tools (if MCP is properly configured, Netdata tools should be available)
134 +
135 +Replace in all examples:
136 +- `YOUR_NETDATA_IP` - IP address or hostname of your Netdata Agent/Parent
137 +- `NETDATA_MCP_API_KEY` - Your [Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key)
138 +- `/usr/sbin/nd-mcp` - With your [actual nd-mcp path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge) (nd-mcp method only)
139 +
140 +## How to Use
141 +
142 +Once configured, Codex CLI can leverage Netdata's observability data for infrastructure analysis:
143 +
144 +```
145 +# Start Codex CLI
146 +codex
147 +
148 +# Ask infrastructure questions
149 +What's the current CPU usage across all servers?
150 +Show me any performance anomalies in the last hour
151 +Which services are consuming the most resources?
152 +```
153 +
154 +## Example Workflows
155 +
156 +**Performance Investigation:**
157 +```
158 +Investigate why our application response times increased this afternoon
159 +```
160 +
161 +**Resource Optimization:**
162 +```
163 +Analyze memory usage patterns and suggest optimization strategies
164 +```
165 +
166 +**Alert Analysis:**
167 +```
168 +Explain the current active alerts and their potential impact
169 +```
170 +
171 +> **💡 Advanced Usage:** Codex CLI can combine observability data with code generation capabilities for powerful DevOps workflows. Learn about the opportunities and security considerations in [AI DevOps Copilot](/docs/ml-ai/ai-devops-copilot/ai-devops-copilot.md).
172 +
173 +## Troubleshooting
174 +
175 +### MCP Server Not Starting
176 +
177 +- Check the command path exists and is executable
178 +- Increase `startup_timeout_sec` for slow-starting servers
179 +- Verify network connectivity to Netdata
180 +
181 +### Connection Timeouts
182 +
183 +- Ensure Netdata is accessible: `curl http://YOUR_NETDATA_IP:19999/api/v3/info`
184 +- Increase timeout values in configuration
185 +- Check firewall rules between Codex CLI and Netdata
186 +
187 +### Limited Data Access
188 +
189 +- Verify the Authorization header is set to `Bearer <your key>`
190 +- Ensure the Netdata agent is properly configured for MCP
191 +- Check that MCP is enabled in your Netdata build
192 +
193 +### Windows Issues
194 +
195 +- MCP servers may have issues on Windows
196 +- Consider using WSL (Windows Subsystem for Linux)
197 +- Check GitHub issues for Windows-specific workarounds
198 +
199 +## Advanced Configuration
200 +
201 +### Multiple Environments
202 +
203 +Configure different Netdata instances for different purposes:
204 +
205 +```toml
206 +# Production environment
207 +[mcp_servers.netdata_prod]
208 +command = "/usr/sbin/nd-mcp"
209 +args = ["ws://prod-parent.company.com:19999/mcp"]
210 +env = { "ND_MCP_BEARER_TOKEN" = "${PROD_API_KEY}" }
211 +startup_timeout_sec = 30
212 +tool_timeout_sec = 120
213 +
214 +[mcp_servers.netdata_staging]
215 +command = "/usr/sbin/nd-mcp"
216 +args = ["ws://staging-parent.company.com:19999/mcp"]
217 +env = { "ND_MCP_BEARER_TOKEN" = "${STAGING_API_KEY}" }
218 +
219 +[mcp_servers.netdata_local]
220 +command = "/usr/sbin/nd-mcp"
221 +args = ["ws://localhost:19999/mcp"]
222 +env = { "ND_MCP_BEARER_TOKEN" = "${LOCAL_API_KEY}" }
223 +```
224 +
225 +### Timeout Configuration
226 +
227 +Adjust timeouts based on your network and query complexity:
228 +
229 +```toml
230 +[mcp_servers.netdata]
231 +command = "npx"
232 +args = [
233 + "mcp-remote@latest",
234 + "--http",
235 + "http://remote-netdata:19999/mcp",
236 + "--allow-http",
237 + "--header",
238 + "Authorization: Bearer NETDATA_MCP_API_KEY"
239 +]
240 +startup_timeout_sec = 30 # Time to wait for MCP server to start
241 +tool_timeout_sec = 180 # Time limit for individual tool calls
242 +```
243 +
244 +## Documentation Links
245 +
246 +- [OpenAI Codex CLI GitHub Repository](https://github.com/openai/codex)
247 +- [Codex CLI Configuration Documentation](https://github.com/openai/codex/blob/main/docs/config.md)
248 +- [Codex CLI Installation Guide](https://github.com/openai/codex#installation)
249 +- [Netdata MCP Setup](/docs/learn/mcp.md)
250 +- [AI DevOps Best Practices](/docs/ml-ai/ai-devops-copilot/ai-devops-copilot.md)
docs/ml-ai/ai-devops-copilot/crush.md new
+367
@@ -0,0 +1,367 @@
1 +# Crush
2 +
3 +Configure Crush by Charmbracelet to access your Netdata infrastructure through MCP for glamorous terminal-based AI operations.
4 +
5 +## Transport Support
6 +
7 +Crush has comprehensive MCP transport support, making it highly flexible for connecting to Netdata:
8 +
9 +| Transport | Support | Use Case |
10 +|-----------|---------|----------|
11 +| **stdio** (via nd-mcp bridge) | ✅ Fully Supported | Local bridge to WebSocket |
12 +| **Streamable HTTP** | ✅ Fully Supported | Direct connection to Netdata's HTTP endpoint |
13 +| **SSE** (Server-Sent Events) | ✅ Fully Supported | Direct connection to Netdata's SSE endpoint |
14 +| **WebSocket** | ❌ Not Supported | Use nd-mcp bridge or HTTP/SSE instead |
15 +
16 +## Prerequisites
17 +
18 +1. **Crush installed** - Available via npm, Homebrew, or direct download from [GitHub](https://github.com/charmbracelet/crush)
19 +2. **The IP and port (usually 19999) of a running Netdata Agent** - Prefer a Netdata Parent to get infrastructure level visibility. Currently the latest nightly version of Netdata has MCP support (not released to the stable channel yet). Your AI Client (running on your desktop or laptop) needs to have direct network access to this IP and port.
20 +3. **For stdio connections only: `nd-mcp` bridge** - The stdio-to-websocket bridge. [Find its absolute path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge). Not needed for direct HTTP/SSE connections.
21 +4. **Optionally, the Netdata MCP API key** that unlocks full access to sensitive observability data (protected functions, full access to logs) on your Netdata. Each Netdata Agent or Parent has its own unique API key for MCP - [Find your Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key)
22 +
23 +> Export `ND_MCP_BEARER_TOKEN` with your MCP key before launching Crush so credentials never appear in command-line arguments or config files:
24 +> ```bash
25 +> export ND_MCP_BEARER_TOKEN="$(cat /var/lib/netdata/mcp_dev_preview_api_key)"
26 +> ```
27 +
28 +## Installation
29 +
30 +Install Crush using one of these methods:
31 +
32 +```bash
33 +# Homebrew (recommended for macOS)
34 +brew install charmbracelet/tap/crush
35 +
36 +# NPM
37 +npm install -g @charmland/crush
38 +
39 +# Arch Linux
40 +yay -S crush-bin
41 +
42 +# Windows (Winget)
43 +winget install charmbracelet.crush
44 +
45 +# Windows (Scoop)
46 +scoop bucket add charm https://github.com/charmbracelet/scoop-bucket.git
47 +scoop install crush
48 +
49 +# Or install with Go
50 +go install github.com/charmbracelet/crush@latest
51 +```
52 +
53 +## Configuration Methods
54 +
55 +Crush uses JSON configuration files with the following priority:
56 +1. `.crush.json` (project-specific)
57 +2. `crush.json` (project-specific)
58 +3. `~/.config/crush/crush.json` (global)
59 +
60 +### Method 1: Direct HTTP Connection (Recommended)
61 +
62 +Connect directly to Netdata's HTTP endpoint without needing the nd-mcp bridge:
63 +
64 +```json
65 +{
66 + "$schema": "https://charm.land/crush.json",
67 + "mcp": {
68 + "netdata": {
69 + "type": "http",
70 + "url": "http://YOUR_NETDATA_IP:19999/mcp",
71 + "headers": {
72 + "Authorization": "Bearer NETDATA_MCP_API_KEY"
73 + },
74 + "timeout": 120,
75 + "disabled": false
76 + }
77 + }
78 +}
79 +```
80 +
81 +For HTTPS connections:
82 +
83 +```json
84 +{
85 + "$schema": "https://charm.land/crush.json",
86 + "mcp": {
87 + "netdata": {
88 + "type": "http",
89 + "url": "https://YOUR_NETDATA_IP:19999/mcp",
90 + "headers": {
91 + "Authorization": "Bearer NETDATA_MCP_API_KEY"
92 + },
93 + "timeout": 120
94 + }
95 + }
96 +}
97 +```
98 +
99 +### Method 2: Direct SSE Connection
100 +
101 +Connect directly to Netdata's SSE endpoint for real-time streaming:
102 +
103 +```json
104 +{
105 + "$schema": "https://charm.land/crush.json",
106 + "mcp": {
107 + "netdata": {
108 + "type": "sse",
109 + "url": "http://YOUR_NETDATA_IP:19999/mcp?transport=sse",
110 + "headers": {
111 + "Authorization": "Bearer NETDATA_MCP_API_KEY"
112 + },
113 + "timeout": 120,
114 + "disabled": false
115 + }
116 + }
117 +}
118 +```
119 +
120 +### Method 3: Using nd-mcp Bridge (stdio)
121 +
122 +For environments where you prefer or need to use the bridge:
123 +
124 +```json
125 +{
126 + "$schema": "https://charm.land/crush.json",
127 + "mcp": {
128 + "netdata": {
129 + "type": "stdio",
130 + "command": "/usr/sbin/nd-mcp",
131 + "args": ["ws://YOUR_NETDATA_IP:19999/mcp"],
132 + "timeout": 120,
133 + "disabled": false
134 + }
135 + }
136 +}
137 +```
138 +
139 +### Method 4: Using npx remote-mcp (Alternative Bridge)
140 +
141 +If nd-mcp is not available, use the official MCP remote client:
142 +
143 +```json
144 +{
145 + "$schema": "https://charm.land/crush.json",
146 + "mcp": {
147 + "netdata": {
148 + "type": "stdio",
149 + "command": "npx",
150 + "args": [
151 + "mcp-remote@latest",
152 + "--http",
153 + "http://YOUR_NETDATA_IP:19999/mcp",
154 + "--allow-http",
155 + "--header",
156 + "Authorization: Bearer NETDATA_MCP_API_KEY"
157 + ],
158 + "timeout": 120
159 + }
160 + }
161 +}
162 +```
163 +
164 +## Environment Variables
165 +
166 +Crush supports environment variable expansion using `$(echo $VAR)` syntax:
167 +
168 +```json
169 +{
170 + "$schema": "https://charm.land/crush.json",
171 + "mcp": {
172 + "netdata": {
173 + "type": "http",
174 + "url": "http://YOUR_NETDATA_IP:19999/mcp",
175 + "headers": {
176 + "Authorization": "Bearer $(echo $NETDATA_API_KEY)"
177 + },
178 + "timeout": 120
179 + }
180 + }
181 +}
182 +```
183 +
184 +## Project-Based Configuration
185 +
186 +Create project-specific configurations by placing `.crush.json` or `crush.json` in your project root:
187 +
188 +```json
189 +{
190 + "$schema": "https://charm.land/crush.json",
191 + "mcp": {
192 + "netdata-prod": {
193 + "type": "http",
194 + "url": "https://prod-parent.company.com:19999/mcp",
195 + "headers": {
196 + "Authorization": "Bearer $(echo $PROD_API_KEY)"
197 + },
198 + "timeout": 120
199 + },
200 + "netdata-staging": {
201 + "type": "sse",
202 + "url": "https://staging-parent.company.com:19999/mcp?transport=sse",
203 + "headers": {
204 + "Authorization": "Bearer $(echo $STAGING_API_KEY)"
205 + },
206 + "timeout": 120
207 + }
208 + }
209 +}
210 +```
211 +
212 +Replace in all examples:
213 +- `YOUR_NETDATA_IP` - IP address or hostname of your Netdata Agent/Parent
214 +- `NETDATA_MCP_API_KEY` - Your [Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key)
215 +- `/usr/sbin/nd-mcp` - With your [actual nd-mcp path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge) (stdio method only)
216 +
217 +## How to Use
218 +
219 +Once configured, start Crush and it will automatically connect to your Netdata MCP servers:
220 +
221 +```bash
222 +# Start Crush
223 +crush
224 +
225 +# Ask infrastructure questions
226 +What's the current CPU usage across all servers?
227 +Show me any performance anomalies in the last hour
228 +Which services are consuming the most resources?
229 +```
230 +
231 +## Tool Permissions
232 +
233 +Crush asks for permission before running tools by default. You can pre-approve certain Netdata tools:
234 +
235 +```json
236 +{
237 + "$schema": "https://charm.land/crush.json",
238 + "permissions": {
239 + "allowed_tools": [
240 + "mcp_netdata_list_metrics",
241 + "mcp_netdata_query_metrics",
242 + "mcp_netdata_list_nodes",
243 + "mcp_netdata_list_alerts"
244 + ]
245 + }
246 +}
247 +```
248 +
249 +> **⚠️ Warning:** Use the `--yolo` flag to bypass all permission prompts, but be extremely careful with this feature.
250 +
251 +## Example Workflows
252 +
253 +**Performance Investigation:**
254 +```
255 +Investigate why our application response times increased this afternoon using Netdata metrics
256 +```
257 +
258 +**Resource Optimization:**
259 +```
260 +Check memory usage patterns across all nodes and suggest optimization strategies
261 +```
262 +
263 +**Alert Analysis:**
264 +```
265 +Explain the current active alerts from Netdata and their potential impact
266 +```
267 +
268 +**Anomaly Detection:**
269 +```
270 +Find any anomalous metrics in the last 2 hours and explain what might be causing them
271 +```
272 +
273 +> **💡 Advanced Usage:** Crush can combine observability data with its terminal-based interface for powerful DevOps workflows. Learn about the opportunities and security considerations in [AI DevOps Copilot](/docs/ml-ai/ai-devops-copilot/ai-devops-copilot.md).
274 +
275 +## Troubleshooting
276 +
277 +### MCP Server Not Connecting
278 +
279 +- Verify Netdata is accessible: `curl http://YOUR_NETDATA_IP:19999/api/v3/info`
280 +- Check the JSON syntax in your configuration file
281 +- Ensure the MCP server is not disabled (`"disabled": false`)
282 +
283 +### Connection Timeouts
284 +
285 +- Increase the `timeout` value in your configuration (default is 120 seconds)
286 +- Check network connectivity between Crush and Netdata
287 +- Verify firewall rules allow access to port 19999
288 +
289 +### Limited Data Access
290 +
291 +- Verify API key is included in the connection URL or headers
292 +- Check that the Netdata agent is properly configured for MCP
293 +- Ensure MCP is enabled in your Netdata build
294 +
295 +### Environment Variable Issues
296 +
297 +- Crush uses `$(echo $VAR)` syntax, not `$VAR` or `${VAR}`
298 +- Ensure environment variables are exported before starting Crush
299 +- Test with `echo $NETDATA_API_KEY` to verify the variable is set
300 +
301 +## Advanced Configuration
302 +
303 +### Multiple Environments with Different Transports
304 +
305 +Configure different Netdata instances using different transport methods:
306 +
307 +```json
308 +{
309 + "$schema": "https://charm.land/crush.json",
310 + "mcp": {
311 + "netdata-local": {
312 + "type": "stdio",
313 + "command": "/usr/sbin/nd-mcp",
314 + "args": ["ws://localhost:19999/mcp"],
315 + "timeout": 60
316 + },
317 + "netdata-parent": {
318 + "type": "http",
319 + "url": "https://parent.company.com:19999/mcp",
320 + "headers": {
321 + "Authorization": "Bearer ${PARENT_API_KEY}"
322 + },
323 + "timeout": 180
324 + },
325 + "netdata-streaming": {
326 + "type": "sse",
327 + "url": "https://stream-parent.company.com:19999/mcp?transport=sse",
328 + "headers": {
329 + "Authorization": "Bearer ${STREAM_API_KEY}"
330 + },
331 + "timeout": 300
332 + }
333 + }
334 +}
335 +```
336 +
337 +> ℹ️ Before switching between environments, export `ND_MCP_BEARER_TOKEN` with the matching key so the bridge authenticates without exposing credentials in the JSON file.
338 +
339 +### Debugging MCP Connections
340 +
341 +Enable debug logging to troubleshoot MCP issues:
342 +
343 +```json
344 +{
345 + "$schema": "https://charm.land/crush.json",
346 + "options": {
347 + "debug": true
348 + }
349 +}
350 +```
351 +
352 +View logs:
353 +```bash
354 +# View recent logs
355 +crush logs
356 +
357 +# Follow logs in real-time
358 +crush logs --follow
359 +```
360 +
361 +## Documentation Links
362 +
363 +- [Crush GitHub Repository](https://github.com/charmbracelet/crush)
364 +- [Crush Configuration Schema](https://charm.land/crush.json)
365 +- [Charmbracelet Documentation](https://charm.sh)
366 +- [Netdata MCP Setup](/docs/learn/mcp.md)
367 +- [AI DevOps Best Practices](/docs/ml-ai/ai-devops-copilot/ai-devops-copilot.md)
docs/ml-ai/ai-devops-copilot/gemini-cli.md
+203 -17
@@ -2,11 +2,22 @@
2
3 Configure Google's Gemini CLI to access your Netdata infrastructure through MCP for powerful AI-driven operations.
4
5 +## Transport Support
6 +
7 +Gemini CLI supports all major MCP transport types, giving you maximum flexibility:
8 +
9 +| Transport | Support | Use Case |
10 +|-----------|---------|----------|
11 +| **stdio** (via nd-mcp bridge) | ✅ Fully Supported | Local bridge to WebSocket |
12 +| **Streamable HTTP** | ✅ Fully Supported | Direct connection to Netdata's HTTP endpoint |
13 +| **SSE** (Server-Sent Events) | ✅ Fully Supported | Direct connection to Netdata's SSE endpoint |
14 +| **WebSocket** | ❌ Not Supported | Use nd-mcp bridge or HTTP/SSE instead |
15 +
16 ## Prerequisites
17
18 1. **Gemini CLI installed** - Available from [GitHub](https://github.com/google-gemini/gemini-cli)
19 2. **The IP and port (usually 19999) of a running Netdata Agent** - Prefer a Netdata Parent to get infrastructure level visibility. Currently the latest nightly version of Netdata has MCP support (not released to the stable channel yet). Your AI Client (running on your desktop or laptop) needs to have direct network access to this IP and port.
9 -3. **`nd-mcp` program available on your desktop or laptop** - This is the bridge that translates `stdio` to `websocket`, connecting your AI Client to your Netdata Agent or Parent. [Find its absolute path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge)
20 +3. **For stdio connections only: `nd-mcp` bridge** - The stdio-to-websocket bridge. [Find its absolute path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge). Not needed for direct HTTP/SSE connections.
21 4. **Optionally, the Netdata MCP API key** that unlocks full access to sensitive observability data (protected functions, full access to logs) on your Netdata. Each Netdata Agent or Parent has its own unique API key for MCP - [Find your Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key)
22
23 ## Installation
@@ -22,38 +33,161 @@ npm install
33 npm run build
34 ```
35
25 -## Configuration
36 +## Configuration Methods
37
38 Gemini CLI has built-in MCP server support. For detailed MCP configuration, see the [official MCP documentation](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md).
39
29 -### Adding Netdata MCP Server
40 +### Method 1: Direct HTTP Connection (Recommended)
41 +
42 +Connect directly to Netdata's HTTP endpoint without needing any bridge:
43 +
44 +```bash
45 +# Using CLI command
46 +gemini mcp add --transport http netdata http://YOUR_NETDATA_IP:19999/mcp \
47 + --header "Authorization: Bearer NETDATA_MCP_API_KEY"
48 +
49 +# For HTTPS connections
50 +gemini mcp add --transport http netdata https://YOUR_NETDATA_IP:19999/mcp \
51 + --header "Authorization: Bearer NETDATA_MCP_API_KEY"
52 +```
53 +
54 +Or configure in `~/.gemini/settings.json`:
55 +
56 +```json
57 +{
58 + "mcpServers": {
59 + "netdata": {
60 + "httpUrl": "http://YOUR_NETDATA_IP:19999/mcp",
61 + "headers": [
62 + "Authorization: Bearer NETDATA_MCP_API_KEY"
63 + ],
64 + "timeout": 30000
65 + }
66 + }
67 +}
68 +```
69 +
70 +### Method 2: Direct SSE Connection
71
31 -Configure your Gemini settings to include the Netdata MCP server:
72 +Connect directly to Netdata's SSE endpoint:
73
74 ```bash
34 -# Edit Gemini settings file
35 -~/.gemini/settings.json
75 +# Using CLI command
76 +gemini mcp add --transport sse netdata http://YOUR_NETDATA_IP:19999/mcp?transport=sse \
77 + --header "Authorization: Bearer NETDATA_MCP_API_KEY"
78 ```
79
38 -Add your Netdata MCP server configuration:
80 +Or configure in `~/.gemini/settings.json`:
81 +
82 +```json
83 +{
84 + "mcpServers": {
85 + "netdata": {
86 + "url": "http://YOUR_NETDATA_IP:19999/mcp?transport=sse",
87 + "headers": [
88 + "Authorization: Bearer NETDATA_MCP_API_KEY"
89 + ],
90 + "timeout": 30000
91 + }
92 + }
93 +}
94 +```
95 +
96 +### Method 3: Using nd-mcp Bridge (stdio)
97 +
98 +For environments where you prefer or need to use the bridge:
99 +
100 +```bash
101 +# Using CLI command
102 +gemini mcp add netdata /usr/sbin/nd-mcp --bearer NETDATA_MCP_API_KEY \
103 + ws://YOUR_NETDATA_IP:19999/mcp
104 +```
105 +
106 +Or configure in `~/.gemini/settings.json`:
107
108 ```json
109 {
110 "mcpServers": {
111 "netdata": {
112 "command": "/usr/sbin/nd-mcp",
45 - "args": ["ws://YOUR_NETDATA_IP:19999/mcp?api_key=NETDATA_MCP_API_KEY"]
113 + "args": [
114 + "--bearer",
115 + "NETDATA_MCP_API_KEY",
116 + "ws://YOUR_NETDATA_IP:19999/mcp"
117 + ],
118 + "timeout": 30000
119 + }
120 + }
121 +}
122 +```
123 +
124 +### Method 4: Using npx remote-mcp (Alternative Bridge)
125 +
126 +If nd-mcp is not available, use the official MCP remote client:
127 +
128 +```bash
129 +# Using CLI command with SSE
130 +gemini mcp add netdata npx mcp-remote@latest \
131 + --sse http://YOUR_NETDATA_IP:19999/mcp \
132 + --allow-http \
133 + --header "Authorization: Bearer NETDATA_MCP_API_KEY"
134 +
135 +# Using HTTP transport
136 +gemini mcp add netdata npx mcp-remote@latest \
137 + --http http://YOUR_NETDATA_IP:19999/mcp \
138 + --allow-http \
139 + --header "Authorization: Bearer NETDATA_MCP_API_KEY"
140 +```
141 +
142 +Or configure in `~/.gemini/settings.json`:
143 +
144 +```json
145 +{
146 + "mcpServers": {
147 + "netdata": {
148 + "command": "npx",
149 + "args": [
150 + "mcp-remote@latest",
151 + "--sse",
152 + "http://YOUR_NETDATA_IP:19999/mcp",
153 + "--allow-http",
154 + "--header",
155 + "Authorization: Bearer NETDATA_MCP_API_KEY",
156 + ]
157 + }
158 + }
159 +}
160 +```
161 +
162 +## Environment Variables
163 +
164 +Gemini CLI supports environment variable expansion in `settings.json`:
165 +- `$VAR_NAME` or `${VAR_NAME}` - Expands to the value of environment variable
166 +
167 +Example configuration with environment variables:
168 +
169 +```json
170 +{
171 + "mcpServers": {
172 + "netdata": {
173 + "httpUrl": "http://${NETDATA_HOST}:19999/mcp",
174 + "headers": [
175 + "Authorization: Bearer ${NETDATA_API_KEY}"
176 + ]
177 }
178 }
179 }
180 ```
181
51 -### Verify MCP Configuration
182 +## Verify MCP Configuration
183
53 -Use the `/mcp` command to verify your setup:
184 +Use these commands to verify your setup:
185
186 ```bash
56 -# List configured MCP servers
187 +# List all configured MCP servers
188 +gemini mcp list
189 +
190 +# Interactive MCP status (within Gemini session)
191 /mcp
192
193 # Show detailed descriptions of MCP servers and tools
@@ -63,11 +197,10 @@ Use the `/mcp` command to verify your setup:
197 /mcp schema
198 ```
199
66 -Replace:
67 -
68 -- `/usr/sbin/nd-mcp` - With your [actual nd-mcp path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge)
200 +Replace in all examples:
201 - `YOUR_NETDATA_IP` - IP address or hostname of your Netdata Agent/Parent
202 - `NETDATA_MCP_API_KEY` - Your [Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key)
203 +- `/usr/sbin/nd-mcp` - With your [actual nd-mcp path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge) (stdio method only)
204
205 ## How to Use
206
@@ -108,11 +241,11 @@ Explain the current active alerts and their potential impact
241
242 - Verify Netdata is accessible: `curl http://YOUR_NETDATA_IP:19999/api/v3/info`
243 - Check that the bridge path exists and is executable
111 -- Ensure API key is correct and properly formatted
244 +- Ensure the Authorization header is correctly formatted
245
246 ### Limited Data Access
247
115 -- Verify API key is included in the connection string
248 +- Verify the Authorization header is present on each request
249 - Check that the Netdata agent is properly configured for MCP
250 - Ensure network connectivity between Gemini CLI and Netdata
251
@@ -122,9 +255,62 @@ Explain the current active alerts and their potential impact
255 - Check MCP server configuration parameters
256 - Verify that MCP protocol is supported in your Gemini CLI installation
257
258 +## Advanced Configuration
259 +
260 +### Multiple Environments
261 +
262 +Configure different Netdata instances for different purposes:
263 +
264 +```json
265 +{
266 + "mcpServers": {
267 + "netdata-prod": {
268 + "httpUrl": "https://prod-parent.company.com:19999/mcp",
269 + "headers": [
270 + "Authorization: Bearer ${PROD_API_KEY}"
271 + ]
272 + },
273 + "netdata-staging": {
274 + "httpUrl": "https://staging-parent.company.com:19999/mcp",
275 + "headers": [
276 + "Authorization: Bearer ${STAGING_API_KEY}"
277 + ]
278 + },
279 + "netdata-local": {
280 + "command": "/usr/sbin/nd-mcp",
281 + "args": [
282 + "--bearer",
283 + "${LOCAL_API_KEY}",
284 + "ws://localhost:19999/mcp"
285 + ]
286 + }
287 + }
288 +}
289 +```
290 +
291 +### Tool Filtering
292 +
293 +Control which Netdata tools are available:
294 +
295 +```json
296 +{
297 + "mcpServers": {
298 + "netdata": {
299 + "httpUrl": "http://YOUR_NETDATA_IP:19999/mcp",
300 + "headers": [
301 + "Authorization: Bearer NETDATA_MCP_API_KEY"
302 + ],
303 + "includeTools": ["query_metrics", "list_alerts", "list_nodes"],
304 + "excludeTools": ["execute_function", "systemd_journal"]
305 + }
306 + }
307 +}
308 +```
309 +
310 ## Documentation Links
311
312 - [Gemini CLI GitHub Repository](https://github.com/google-gemini/gemini-cli)
128 -- [Gemini CLI Official Documentation](https://developers.google.com/gemini-code-assist/docs/gemini-cli)
313 +- [Gemini CLI MCP Documentation](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md)
314 +- [Gemini CLI Configuration Guide](https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/configuration.md)
315 - [Netdata MCP Setup](/docs/learn/mcp.md)
316 - [AI DevOps Best Practices](/docs/ml-ai/ai-devops-copilot/ai-devops-copilot.md)
docs/ml-ai/ai-devops-copilot/opencode.md new
+329
@@ -0,0 +1,329 @@
1 +# OpenCode
2 +
3 +Configure SST's OpenCode to access your Netdata infrastructure through MCP for terminal-based AI-powered DevOps operations.
4 +
5 +## Transport Support
6 +
7 +OpenCode supports both local and remote MCP servers:
8 +
9 +| Transport | Support | Use Case |
10 +|-----------|---------|----------|
11 +| **stdio** (local) | ✅ Fully Supported | Local servers via nd-mcp bridge |
12 +| **Streamable HTTP** (remote) | ✅ Fully Supported | Direct connection to Netdata's HTTP endpoint |
13 +| **SSE** (Server-Sent Events) | ⚠️ Limited Support | Known issues with SSE servers |
14 +| **WebSocket** | ❌ Not Supported | Use nd-mcp bridge or HTTP instead |
15 +
16 +> **Note:** OpenCode has reported issues with SSE-based MCP servers ([GitHub Issue #834](https://github.com/sst/opencode/issues/834)). Use HTTP streamable transport for best compatibility.
17 +
18 +## Prerequisites
19 +
20 +1. **OpenCode installed** - Available via npm, brew, or direct download from [GitHub](https://github.com/sst/opencode)
21 +2. **The IP and port (usually 19999) of a running Netdata Agent** - Prefer a Netdata Parent to get infrastructure level visibility. Currently the latest nightly version of Netdata has MCP support (not released to the stable channel yet). Your AI Client (running on your desktop or laptop) needs to have direct network access to this IP and port.
22 +3. **For local connections only: `nd-mcp` bridge** - The stdio-to-websocket bridge. [Find its absolute path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge). Not needed for remote HTTP connections.
23 +4. **Optionally, the Netdata MCP API key** that unlocks full access to sensitive observability data (protected functions, full access to logs) on your Netdata. Each Netdata Agent or Parent has its own unique API key for MCP - [Find your Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key)
24 +
25 +> Export `ND_MCP_BEARER_TOKEN` with your MCP key before launching OpenCode to keep secrets out of configuration files:
26 +> ```bash
27 +> export ND_MCP_BEARER_TOKEN="$(cat /var/lib/netdata/mcp_dev_preview_api_key)"
28 +> ```
29 +
30 +## Installation
31 +
32 +Install OpenCode using one of these methods:
33 +
34 +```bash
35 +# Using npm (recommended)
36 +npm i -g opencode-ai@latest
37 +
38 +# Using Homebrew
39 +brew install sst/tap/opencode
40 +
41 +# Using curl installation script
42 +curl -fsSL https://opencode.ai/install.sh | bash
43 +```
44 +
45 +## Configuration Methods
46 +
47 +OpenCode uses an `opencode.json` configuration file with MCP servers defined under the `mcp` key.
48 +
49 +### Method 1: Direct HTTP Connection (Recommended)
50 +
51 +Connect directly to Netdata's HTTP endpoint without needing the nd-mcp bridge:
52 +
53 +```json
54 +{
55 + "mcp": {
56 + "netdata": {
57 + "type": "remote",
58 + "url": "http://YOUR_NETDATA_IP:19999/mcp",
59 + "headers": {
60 + "Authorization": "Bearer NETDATA_MCP_API_KEY"
61 + },
62 + "enabled": true
63 + }
64 + }
65 +}
66 +```
67 +
68 +For HTTPS connections:
69 +
70 +```json
71 +{
72 + "mcp": {
73 + "netdata": {
74 + "type": "remote",
75 + "url": "https://YOUR_NETDATA_IP:19999/mcp",
76 + "headers": {
77 + "Authorization": "Bearer NETDATA_MCP_API_KEY"
78 + },
79 + "enabled": true
80 + }
81 + }
82 +}
83 +```
84 +
85 +### Method 2: Using nd-mcp Bridge (Local)
86 +
87 +For environments where you prefer or need to use the bridge:
88 +
89 +```json
90 +{
91 + "mcp": {
92 + "netdata": {
93 + "type": "local",
94 + "command": ["/usr/sbin/nd-mcp", "ws://YOUR_NETDATA_IP:19999/mcp"],
95 + "enabled": true
96 + }
97 + }
98 +}
99 +```
100 +
101 +### Method 3: Using npx remote-mcp (Alternative Bridge)
102 +
103 +If nd-mcp is not available, use the official MCP remote client:
104 +
105 +```json
106 +{
107 + "mcp": {
108 + "netdata": {
109 + "type": "local",
110 + "command": [
111 + "npx",
112 + "mcp-remote@latest",
113 + "--http",
114 + "http://YOUR_NETDATA_IP:19999/mcp",
115 + "--allow-http",
116 + "--header",
117 + "Authorization: Bearer NETDATA_MCP_API_KEY"
118 + ],
119 + "enabled": true
120 + }
121 + }
122 +}
123 +```
124 +
125 +## Environment Variables
126 +
127 +OpenCode supports environment variables in local server configurations:
128 +
129 +```json
130 +{
131 + "mcp": {
132 + "netdata": {
133 + "type": "local",
134 + "command": ["/usr/sbin/nd-mcp", "ws://YOUR_NETDATA_IP:19999/mcp"],
135 + "enabled": true,
136 + "environment": {
137 + "ND_MCP_BEARER_TOKEN": "your-api-key-here"
138 + }
139 + }
140 + }
141 +}
142 +```
143 +
144 +For remote servers with environment variables:
145 +
146 +```json
147 +{
148 + "mcp": {
149 + "netdata": {
150 + "type": "remote",
151 + "url": "https://YOUR_NETDATA_IP:19999/mcp",
152 + "headers": {
153 + "Authorization": "Bearer ${NETDATA_API_KEY}"
154 + },
155 + "enabled": true
156 + }
157 + }
158 +}
159 +```
160 +
161 +Replace in all examples:
162 +- `YOUR_NETDATA_IP` - IP address or hostname of your Netdata Agent/Parent
163 +- `ND_MCP_BEARER_TOKEN` - Export with your [Netdata MCP API key](/docs/learn/mcp.md#finding-your-api-key) before launching OpenCode
164 +- `/usr/sbin/nd-mcp` - With your [actual nd-mcp path](/docs/learn/mcp.md#finding-the-nd-mcp-bridge) (local method only)
165 +
166 +## How to Use
167 +
168 +Once configured, OpenCode can leverage Netdata's observability data through its terminal interface:
169 +
170 +```bash
171 +# Start OpenCode
172 +opencode
173 +
174 +# The AI assistant will have access to Netdata tools
175 +# Ask infrastructure questions naturally:
176 +What's the current CPU usage across all servers?
177 +Show me any performance anomalies in the last hour
178 +Which services are consuming the most resources?
179 +```
180 +
181 +## Selective Tool Enabling
182 +
183 +OpenCode allows fine-grained control over MCP tool availability per agent:
184 +
185 +```json
186 +{
187 + "mcp": {
188 + "netdata": {
189 + "type": "remote",
190 + "url": "http://YOUR_NETDATA_IP:19999/mcp",
191 + "headers": {
192 + "Authorization": "Bearer NETDATA_MCP_API_KEY"
193 + },
194 + "enabled": true
195 + }
196 + },
197 + "tools": {
198 + "netdata*": false
199 + },
200 + "agent": {
201 + "infrastructure-analyst": {
202 + "tools": {
203 + "netdata*": true
204 + }
205 + }
206 + }
207 +}
208 +```
209 +
210 +This configuration:
211 +- Disables Netdata tools globally
212 +- Enables them only for the "infrastructure-analyst" agent
213 +
214 +## Example Workflows
215 +
216 +**Performance Investigation:**
217 +```
218 +Investigate why our application response times increased this afternoon using Netdata metrics
219 +```
220 +
221 +**Resource Optimization:**
222 +```
223 +Check memory usage patterns across all nodes and suggest optimization strategies
224 +```
225 +
226 +**Alert Analysis:**
227 +```
228 +Explain the current active alerts from Netdata and their potential impact
229 +```
230 +
231 +**Anomaly Detection:**
232 +```
233 +Find any anomalous metrics in the last 2 hours and explain what might be causing them
234 +```
235 +
236 +> **💡 Advanced Usage:** OpenCode's terminal-based interface combined with Netdata observability creates powerful DevOps workflows. Learn about the opportunities and security considerations in [AI DevOps Copilot](/docs/ml-ai/ai-devops-copilot/ai-devops-copilot.md).
237 +
238 +## Troubleshooting
239 +
240 +### MCP Server Not Connecting
241 +
242 +- Verify Netdata is accessible: `curl http://YOUR_NETDATA_IP:19999/api/v3/info`
243 +- Check the JSON syntax in your `opencode.json` file
244 +- Ensure the MCP server is enabled (`"enabled": true`)
245 +
246 +### SSE Transport Issues
247 +
248 +OpenCode has known issues with SSE-based MCP servers. If you encounter "UnknownError Server error" messages:
249 +- Switch to HTTP streamable transport (remove `?transport=sse` from URL)
250 +- Use the local nd-mcp bridge instead
251 +- Check [GitHub Issue #834](https://github.com/sst/opencode/issues/834) for updates
252 +
253 +### Limited Data Access
254 +
255 +- Verify API key is included in the connection URL or headers
256 +- Check that the Netdata agent is properly configured for MCP
257 +- Ensure MCP is enabled in your Netdata build
258 +
259 +### Command Format Issues
260 +
261 +- Local servers require command as an array: `["command", "arg1", "arg2"]`
262 +- Remote servers use a URL string: `"url": "http://..."`
263 +- Don't mix local and remote configuration options
264 +
265 +## Advanced Configuration
266 +
267 +### Multiple Environments
268 +
269 +Configure different Netdata instances for different purposes:
270 +
271 +```json
272 +{
273 + "mcp": {
274 + "netdata-prod": {
275 + "type": "remote",
276 + "url": "https://prod-parent.company.com:19999/mcp",
277 + "headers": {
278 + "Authorization": "Bearer ${PROD_API_KEY}"
279 + },
280 + "enabled": true
281 + },
282 + "netdata-staging": {
283 + "type": "remote",
284 + "url": "https://staging-parent.company.com:19999/mcp",
285 + "headers": {
286 + "Authorization": "Bearer ${STAGING_API_KEY}"
287 + },
288 + "enabled": false
289 + },
290 + "netdata-local": {
291 + "type": "local",
292 + "command": ["/usr/sbin/nd-mcp", "ws://localhost:19999/mcp"],
293 + "environment": {
294 + "ND_MCP_BEARER_TOKEN": "${LOCAL_API_KEY}"
295 + },
296 + "enabled": true
297 + }
298 + }
299 +}
300 +```
301 +
302 +### Debugging MCP Connections
303 +
304 +Enable verbose logging to troubleshoot MCP issues:
305 +
306 +```json
307 +{
308 + "mcp": {
309 + "netdata": {
310 + "type": "remote",
311 + "url": "http://YOUR_NETDATA_IP:19999/mcp",
312 + "headers": {
313 + "Authorization": "Bearer NETDATA_MCP_API_KEY"
314 + },
315 + "enabled": true,
316 + "debug": true
317 + }
318 + }
319 +}
320 +```
321 +
322 +## Documentation Links
323 +
324 +- [OpenCode GitHub Repository](https://github.com/sst/opencode)
325 +- [OpenCode Documentation](https://opencode.ai/docs)
326 +- [OpenCode MCP Servers Guide](https://opencode.ai/docs/mcp-servers/)
327 +- [SST Discord Community](https://discord.gg/sst)
328 +- [Netdata MCP Setup](/docs/learn/mcp.md)
329 +- [AI DevOps Best Practices](/docs/ml-ai/ai-devops-copilot/ai-devops-copilot.md)
src/daemon/main.c
+2
@@ -6,6 +6,7 @@
6 #include "status-file.h"
7 #include "static_threads.h"
8 #include "web/api/queries/backfill.h"
9 +#include "web/mcp/mcp.h"
10
11 #include "database/engine/page_test.h"
12 #include <curl/curl.h>
@@ -945,6 +946,7 @@ int netdata_main(int argc, char **argv) {
946 // get the certificate and start security
947 netdata_conf_web_security_init();
948 nd_web_api_init();
949 + mcp_initialize_subsystem();
950 web_server_threading_selection();
951
952 delta_startup_time("web server sockets");
src/libnetdata/http/content_type.c
+1
@@ -12,6 +12,7 @@ static struct {
12 // primary - preferred during id-to-string conversions
13 { .format = "application/json", CT_APPLICATION_JSON, true },
14 { .format = "text/plain", CT_TEXT_PLAIN, true },
15 + { .format = "text/event-stream", CT_TEXT_EVENT_STREAM, true },
16 { .format = "text/html", CT_TEXT_HTML, true },
17 { .format = "text/css", CT_TEXT_CSS, true },
18 { .format = "text/yaml", CT_TEXT_YAML, true },
src/libnetdata/http/content_type.h
+1
@@ -7,6 +7,7 @@ typedef enum __attribute__ ((__packed__)) {
7 CT_NONE = 0,
8 CT_APPLICATION_JSON,
9 CT_TEXT_PLAIN,
10 + CT_TEXT_EVENT_STREAM,
11 CT_TEXT_HTML,
12 CT_APPLICATION_X_JAVASCRIPT,
13 CT_TEXT_CSS,
src/web/api/http_auth.c
+8
@@ -1,6 +1,7 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #include "http_auth.h"
4 +#include "web/api/mcp_auth.h"
5
6 #define BEARER_TOKEN_EXPIRATION (86400 * 1)
7
@@ -306,6 +307,13 @@ bool web_client_bearer_token_auth(struct web_client *w, const char *v) {
307 if(!v || !*v || strcmp(v, "null") == 0 || strcmp(v, "undefined") == 0)
308 return rc;
309
310 +#ifdef NETDATA_MCP_DEV_PREVIEW_API_KEY
311 + if (mcp_api_key_verify(v)) {
312 + web_client_set_mcp_preview_key(w);
313 + return true;
314 + }
315 +#endif
316 +
317 if(!uuid_parse_flexi(v, w->auth.bearer_token)) {
318 char uuid_str[UUID_COMPACT_STR_LEN];
319 uuid_unparse_lower_compact(w->auth.bearer_token, uuid_str);
src/web/api/http_header.c
+40
@@ -2,6 +2,9 @@
2
3 #include "http_header.h"
4
5 +#include <string.h>
6 +#include <strings.h>
7 +
8 static void web_client_enable_deflate(struct web_client *w, bool gzip) {
9 if(gzip)
10 web_client_flag_set(w, WEB_CLIENT_ENCODING_GZIP);
@@ -82,6 +85,42 @@ static void http_header_user_agent(struct web_client *w, const char *v, size_t l
85 }
86 }
87
88 +static void http_header_accept(struct web_client *w, const char *v, size_t len __maybe_unused) {
89 + web_client_flag_clear(w, WEB_CLIENT_FLAG_ACCEPT_JSON |
90 + WEB_CLIENT_FLAG_ACCEPT_SSE |
91 + WEB_CLIENT_FLAG_ACCEPT_TEXT);
92 +
93 + for (const char *p = v; p && *p; ) {
94 + while (*p == ' ' || *p == '\t' || *p == ',') p++;
95 + if (!*p)
96 + break;
97 +
98 + const char *start = p;
99 + while (*p && *p != ',' && *p != ';')
100 + p++;
101 + size_t length = (size_t)(p - start);
102 +
103 + while (*p && *p != ',')
104 + p++;
105 +
106 + if (length == 0)
107 + continue;
108 +
109 + if (length >= strlen("application/json") &&
110 + strncasecmp(start, "application/json", strlen("application/json")) == 0) {
111 + web_client_flag_set(w, WEB_CLIENT_FLAG_ACCEPT_JSON);
112 + }
113 + else if (length >= strlen("text/event-stream") &&
114 + strncasecmp(start, "text/event-stream", strlen("text/event-stream")) == 0) {
115 + web_client_flag_set(w, WEB_CLIENT_FLAG_ACCEPT_SSE);
116 + }
117 + else if (length >= strlen("text/plain") &&
118 + strncasecmp(start, "text/plain", strlen("text/plain")) == 0) {
119 + web_client_flag_set(w, WEB_CLIENT_FLAG_ACCEPT_TEXT);
120 + }
121 + }
122 +}
123 +
124 static void http_header_x_auth_token(struct web_client *w, const char *v, size_t len __maybe_unused) {
125 freez(w->auth_bearer_token);
126 w->auth_bearer_token = strdupz(v);
@@ -302,6 +341,7 @@ struct {
341 { .hash = 0, .key = "Connection", .cb = http_header_connection },
342 { .hash = 0, .key = "DNT", .cb = http_header_dnt },
343 { .hash = 0, .key = "User-Agent", .cb = http_header_user_agent},
344 + { .hash = 0, .key = "Accept", .cb = http_header_accept },
345 { .hash = 0, .key = "X-Auth-Token", .cb = http_header_x_auth_token },
346 { .hash = 0, .key = "Host", .cb = http_header_host },
347 { .hash = 0, .key = "Accept-Encoding", .cb = http_header_accept_encoding },
src/web/api/mcp_auth.c renamed
+3 -3
@@ -1,6 +1,6 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -#include "mcp-api-key.h"
3 +#include "mcp_auth.h"
4 #include "claim/claim.h"
5 #include <fcntl.h>
6 #include <sys/stat.h>
@@ -111,7 +111,7 @@ void mcp_api_key_initialize(void) {
111 return;
112 }
113 }
114 -
114 +
115 char path[PATH_MAX];
116 snprintf(path, sizeof(path), "%s/%s", netdata_configured_varlib_dir, MCP_DEV_PREVIEW_API_KEY_FILENAME);
117 netdata_log_info("MCP: Developer preview API key initialized. Location: %s", path);
@@ -149,4 +149,4 @@ const char *mcp_api_key_get(void) {
149 return mcp_dev_preview_api_key;
150 }
151
152 -#endif // NETDATA_MCP_DEV_PREVIEW_API_KEY
\ No newline at end of file
152 +#endif // NETDATA_MCP_DEV_PREVIEW_API_KEY
src/web/api/mcp_auth.h renamed
+3 -3
@@ -1,7 +1,7 @@
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 -#ifndef NETDATA_MCP_API_KEY_H
4 -#define NETDATA_MCP_API_KEY_H
3 +#ifndef NETDATA_MCP_AUTH_H
4 +#define NETDATA_MCP_AUTH_H
5
6 #include "daemon/common.h"
7
@@ -25,4 +25,4 @@ const char *mcp_api_key_get(void);
25
26 #endif // NETDATA_MCP_DEV_PREVIEW_API_KEY
27
28 -#endif // NETDATA_MCP_API_KEY_H
\ No newline at end of file
28 +#endif // NETDATA_MCP_AUTH_H
src/web/mcp/README.md
+49 -13
@@ -21,7 +21,9 @@ You can use Netdata with the following AI assistants:
21
22 Probably more: Check the [MCP documentation](https://modelcontextprotocol.io/clients) for a full list of supported AI assistants.
23
24 -All these AI assistants need local access to the MCP servers. This means that the application you run locally on your computer (Claude Desktop, Cursor, etc) needs to be able to connect to the Netdata using `stdio` communication. However, since your Netdata runs remotely on a server, you need a bridge to convert the `stdio` communication to `WebSocket` communication. Netdata provides bridges in multiple languages (Node.js, Python, Go) to facilitate this.
24 +All these AI assistants need local access to the MCP servers. When the client supports **HTTP streamable** or **Server-Sent Events (SSE)** transports (for example, `npx @modelcontextprotocol/remote-mcp`), it can now connect directly to Netdata's `/mcp` (HTTP) or `/sse` endpoints—no custom bridge required.
25 +
26 +Many desktop assistants, however, still talk to MCP servers over `stdio`. For them you still need a bridge that converts `stdio` to a network transport. Netdata keeps shipping the `nd-mcp` bridge (plus the polyglot bridges in `bridges/`) for this purpose.
27
28 Once MCP is integrated into Netdata Cloud, Web-based AI assistants will also be supported. For Web-based AI assistants, the backend of the assistant connects to a publicly accessible MCP server (i.e. Netdata Cloud) to access infrastructure observability data, without needing a bridge.
29
@@ -41,14 +43,16 @@ The configuration of most AI assistants is done via a configuration file, which
43 "netdata": {
44 "command": "/usr/bin/nd-mcp",
45 "args": [
44 - "ws://IP_OF_YOUR_NETDATA:19999/mcp?api_key=YOUR_API_KEY"
46 + "--bearer",
47 + "YOUR_API_KEY",
48 + "ws://IP_OF_YOUR_NETDATA:19999/mcp"
49 ]
50 }
51 }
52 }
53 ```
54
51 -The program `nd-mcp` is the bridge program that converts `stdio` communication to `WebSocket` communication. This program is part of all Netdata installations, so by installing Netdata on your personal computer (Linux, MacOS, Windows) you will have it available.
55 +The program `nd-mcp` is still the universal bridge that converts `stdio` communication to network transports. This program is part of all Netdata installations, so by installing Netdata on your personal computer (Linux, macOS, Windows) you will have it available.
56
57 There may be different paths for it, depending on how you installed Netdata:
58
@@ -57,6 +61,33 @@ There may be different paths for it, depending on how you installed Netdata:
61 - `/usr/local/netdata/usr/bin/nd-mcp`: MacOS installations from source
62 - `C:\\Program Files\\Netdata\\usr\\bin\\nd-mcp.exe`: Windows installations
63
64 +### Native HTTP/SSE connection (remote-mcp)
65 +
66 +If your client supports HTTP or SSE, you can skip the bridge entirely. The Netdata agent exposes two MCP HTTP endpoints on the same port as the dashboard:
67 +
68 +| Endpoint | Transport | Notes |
69 +| --- | --- | --- |
70 +| `http://IP_OF_YOUR_NETDATA:19999/mcp` | Streamable HTTP (chunked JSON) | Default response; add `Accept: application/json` |
71 +| `http://IP_OF_YOUR_NETDATA:19999/mcp?transport=sse` | Server-Sent Events | Equivalent to sending `Accept: text/event-stream` |
72 +
73 +To test quickly with the official MCP CLI:
74 +
75 +```bash
76 +npx @modelcontextprotocol/remote-mcp \
77 + --sse http://IP_OF_YOUR_NETDATA:19999/mcp \
78 + --header "Authorization: Bearer YOUR_API_KEY"
79 +```
80 +
81 +Or, to prefer streamable HTTP:
82 +
83 +```bash
84 +npx @modelcontextprotocol/remote-mcp \
85 + --http http://IP_OF_YOUR_NETDATA:19999/mcp \
86 + --header "Authorization: Bearer YOUR_API_KEY"
87 +```
88 +
89 +These commands let you browse the Netdata MCP tools without installing `nd-mcp`. You can still keep `nd-mcp` in your assistant configuration as a fallback for clients that only speak `stdio`.
90 +
91 You will also need:
92
93 `IP_OF_YOUR_NETDATA`, is the IP address or hostname of the Netdata instance you want to connect to. This will eventually be replaced by the Netdata Cloud URL. For this dev preview, use any Netdata, preferably one of your parent nodes. Remember that the AI assistant will "see" only the nodes that are connected to that Netdata instance.
@@ -112,7 +143,7 @@ For [Claude Code](https://claude.ai/code), add to your project's root, the file
143 Alternatively, you can add it using a Claude CLI command like this:
144
145 ```bash
115 -claude mcp add netdata /usr/bin/nd-mcp ws://IP_OF_YOUR_NETDATA:19999/mcp?api_key=YOUR_API_KEY
146 +claude mcp add netdata /usr/bin/nd-mcp --bearer YOUR_API_KEY ws://IP_OF_YOUR_NETDATA:19999/mcp
147 ```
148
149 Once configured correctly, run `claude mcp list` or you can issue the command `/mcp` to your Claude Code. It should show you the available MCP servers, including "netdata".
@@ -122,6 +153,7 @@ Once configured correctly, run `claude mcp list` or you can issue the command `/
153 For [Cursor](https://www.cursor.com/), add the configuration to the MCP settings.
154
155 ## Alternative `stdio` to `websocket` Bridges
156 +These bridges remain useful for AI assistants that only support `stdio`. If your tooling can use Netdata's native HTTP/SSE endpoints you can skip this section.
157
158 We provide 3 different bridges for you to choose the one that best fits your environment:
159
@@ -268,7 +300,7 @@ Once configured, you can ask questions like:
300 - A: Yes, MCP supports multiple AI assistants. Check the [MCP documentation](https://modelcontextprotocol.io/clients) for a full list.
301
302 - **Q: Do I need to run a bridge on my local machine?**
271 -- A: Yes, the bridge converts `stdio` communication to `WebSocket` for remote access to Netdata. The bridge is run on your local machine (personal computer) to connect to the Netdata instance.
303 +- A: Only if your client speaks `stdio` (Claude Desktop, Cursor, etc). Modern MCP clients such as `npx @modelcontextprotocol/remote-mcp` can talk HTTP/SSE directly to Netdata's `/mcp` endpoints, so no bridge is required in that case. Keep `nd-mcp` as a fallback for assistants that still require `stdio`.
304
305 - **Q: How do I find my API key?**
306 - A: The API key is automatically generated by Netdata and stored in `/var/lib/netdata/mcp_dev_preview_api_key` or `/opt/netdata/var/lib/netdata/mcp_dev_preview_api_key` on the Netdata Agent you will connect to. Use `sudo cat` to view it.
@@ -331,16 +363,20 @@ If you need to configure multiple MCP servers, you can add them under the `mcpSe
363 {
364 "mcpServers": {
365 "netdata-production": {
334 - "command": "/usr/bin/nd-mcp",
335 - "args": [
336 - "ws://IP_OF_YOUR_NETDATA:19999/mcp?api_key=YOUR_API_KEY"
337 - ]
366 + "command": "/usr/bin/nd-mcp",
367 + "args": [
368 + "--bearer",
369 + "YOUR_API_KEY",
370 + "ws://IP_OF_YOUR_NETDATA:19999/mcp"
371 + ]
372 },
373 "netdata-testing": {
340 - "command": "/usr/bin/nd-mcp",
341 - "args": [
342 - "ws://IP_OF_YOUR_NETDATA:19999/mcp?api_key=YOUR_API_KEY"
343 - ]
374 + "command": "/usr/bin/nd-mcp",
375 + "args": [
376 + "--bearer",
377 + "YOUR_API_KEY",
378 + "ws://IP_OF_YOUR_NETDATA:19999/mcp"
379 + ]
380 }
381 }
382 }
src/web/mcp/TODO-LIST.md
+72 -64
@@ -13,6 +13,32 @@ This document outlines the complete plan for implementing the Model Context Prot
13 4. **Multi-buffer responses** - Support ordered responses using libnetdata double-linked lists
14 5. **Clean job-based execution** - Each request becomes a structured job
15
16 +## Phase 1 – Transport Decoupling (Current Focus)
17 +
18 +### Goals
19 +- Keep request parsing inside each adapter while handing a parsed `json_object *` to the core. [done]
20 +- Transform `MCP_CLIENT` into a session container with a per-request array of `BUFFER *` chunks instead of a single result buffer and JSON-RPC metadata. [done]
21 +- Provide helper APIs (e.g. `mcp_response_reset`, `mcp_response_add_json`, `mcp_response_add_text`, `mcp_response_finalize`) so namespace handlers build transport-neutral responses without touching envelopes. [done]
22 +- Ensure adapters own correlation data: WebSocket keeps JSON-RPC ids, future transports can pick their own tokens. [done]
23 +- Preserve existing namespace function signatures by passing the same `MCP_CLIENT *`, params object, and `MCP_REQUEST_ID` while changing only the response building helpers they call. [done]
24 +
25 +### Deliverables
26 +- Response buffer management implementation with request-level limits and ownership handled by `MCP_CLIENT`. [done]
27 +- Updated namespace implementations (initialize, ping, tools, resources, prompts, logging, completion, etc.) to use the new helper APIs. [done]
28 +- WebSocket adapter refactor that wraps/unwraps JSON-RPC entirely in adapter code, including batching and notifications. [done]
29 +- Documentation updates describing the new lifecycle and expectations for adapters. [done]
30 +
31 +### Open Questions / Checks
32 +- Confirm memory caps for accumulated response buffers and expose configuration knobs if required. [done]
33 +- Validate streaming semantics: adapters must never split a single `BUFFER`, but may send multiple buffers sequentially. [done]
34 +- Identify any shared utilities (UUID helpers, auth context) that should remain in core versus adapter. [done]
35 +
36 +Status:
37 +- [x] Response buffer helpers implemented in mcp.c (prepare, add_json/text, finalize via buffer_json_finalize in handlers)
38 +- [x] Namespaces updated to use helpers (initialize, ping, tools, resources, prompts, logging, completion)
39 +- [x] WebSocket adapter wraps JSON-RPC (batching, notifications) and converts MCP response chunks to JSON-RPC payloads
40 +- [x] Error handling unified via mcp_error_result and mcpc->error buffer
41 +
42 ## 1. Core MCP Architecture Refactoring
43
44 ### A. Job-Based Request Processing
@@ -171,61 +197,54 @@ const MCP_TOOL_REGISTRY_ENTRY **mcp_get_tools_by_namespace(MCP_NAMESPACE namespa
197
198 ### A. HTTP Adapter (Integrated with Netdata Web Server)
199
174 -#### HTTP Route Registration
200 +#### HTTP Routing Hooks
201 ```c
176 -// HTTP adapter decides its own URL structure
177 -int mcp_http_adapter_init_routes(void) {
178 - // Direct tool execution endpoints
179 - web_client_api_request_v3_register("/api/v3/mcp/execute_function", mcp_http_handle_execute_function);
180 - web_client_api_request_v3_register("/api/v3/mcp/query_metrics", mcp_http_handle_query_metrics);
181 -
182 - // Generic endpoints using registry
183 - web_client_api_request_v3_register("/api/v3/mcp/tools", mcp_http_handle_tools_list);
184 - web_client_api_request_v3_register("/api/v3/mcp/tools/*/call", mcp_http_handle_tool_call);
185 - web_client_api_request_v3_register("/api/v3/mcp/tools/*/schema", mcp_http_handle_tool_schema);
186 -
187 - return 0;
202 +// src/web/server/web_client.c
203 +else if (unlikely(hash == hash_mcp && strcmp(tok, "mcp") == 0)) {
204 + if (!http_can_access_dashboard(w))
205 + return web_client_permission_denied_acl(w);
206 + return mcp_http_handle_request(host, w);
207 +}
208 +else if (unlikely(hash == hash_sse && strcmp(tok, "sse") == 0)) {
209 + if (!http_can_access_dashboard(w))
210 + return web_client_permission_denied_acl(w);
211 + return mcp_sse_handle_request(host, w);
212 }
213 ```
214
191 -#### Authorization Integration (Following Netdata Pattern Exactly)
215 +`mcp_http_handle_request()` streams the accumulated MCP response as JSON (chunked when multiple buffers are present). `mcp_sse_handle_request()` produces Server-Sent Event frames and disables compression before returning.
216 +
217 +#### Authorization Integration
218 ```c
193 -// Generic tool execution using registry (like web_client_api_request_vX)
194 -int mcp_http_handle_tool_call(RRDHOST *host, struct web_client *w, char *url) {
195 - const char *tool_name = extract_tool_name_from_url(url);
196 -
197 - // Look up in registry
198 - const MCP_TOOL_REGISTRY_ENTRY *tool = mcp_find_tool(tool_name);
199 - if (!tool) {
200 - return web_client_api_request_v1_info_fill_buffer(host, w, "Tool not found");
201 - }
202 -
203 - // Check ACL and access (following Netdata pattern exactly)
204 - if(tool->acl != HTTP_ACL_NOCHECK) {
205 - if(!(w->acl & tool->acl)) {
206 - web_client_permission_denied_acl(w);
207 - return HTTP_RESP_FORBIDDEN;
208 - }
209 -
210 - if(tool->access != HTTP_ACCESS_NONE) {
211 - if(!web_client_can_access_with_auth(w, tool->access)) {
212 - web_client_permission_denied_access(w, tool->access);
213 - return HTTP_ACCESS_PERMISSION_DENIED_HTTP_CODE(tool->access);
214 - }
215 - }
216 - }
217 -
218 - // Execute tool
219 - // ... implementation
219 +static inline bool mcp_adapter_authorize(struct web_client *w, const MCP_TOOL_REGISTRY_ENTRY *tool) {
220 + if (!tool)
221 + return false;
222 + if (tool->acl != HTTP_ACL_NOCHECK && !(w->acl & tool->acl))
223 + return false;
224 + if (tool->access != HTTP_ACCESS_NONE && !web_client_can_access_with_auth(w, tool->access))
225 + return false;
226 + return true;
227 +}
228 +
229 +int mcp_http_handle_request(RRDHOST *host, struct web_client *w) {
230 + struct json_object *request = mcp_http_parse_request_body(w);
231 + const char *method = mcp_http_request_method(request);
232 + const MCP_TOOL_REGISTRY_ENTRY *tool = mcp_find_tool(method);
233 + if (!mcp_adapter_authorize(w, tool))
234 + return web_client_permission_denied_acl(w);
235 +
236 + MCP_CLIENT *mcpc = mcp_create_client(MCP_TRANSPORT_HTTP, w);
237 + MCP_RETURN_CODE rc = mcp_dispatch_method(mcpc, method, mcp_http_request_params(request), 1);
238 + return mcp_http_send_response(w, mcpc, rc);
239 }
240 ```
241
242 **Status**:
224 -- [ ] Implement HTTP route registration
225 -- [ ] Implement HTTP request parsing (JSON body to params)
226 -- [ ] Implement HTTP response conversion (BUFFER list to HTTP JSON)
227 -- [ ] Integrate with existing Netdata authorization system
228 -- [ ] Add HTTP-specific error handling
243 +- [ ] Add `/mcp` and `/sse` branches in `web_client_process_url()`
244 +- [ ] Implement HTTP JSON parsing helpers (`mcp_http_parse_request_body`, etc.)
245 +- [ ] Implement chunked JSON serializer (`mcp_http_send_response`)
246 +- [ ] Implement SSE serializer (`mcp_sse_send_response`)
247 +- [ ] Share authorization helpers between HTTP and SSE adapters
248
249 ### B. WebSocket/JSON-RPC Adapter (Manages MCP_CLIENT)
250
@@ -344,7 +363,8 @@ src/web/mcp/
363 │ │ ├── mcp-jsonrpc-adapter.c/h # tools/list, tools/call implementation
364 │ │ └── mcp-client.c/h # MCP_CLIENT management
365 │ └── http/
347 -│ └── mcp-http-adapter.c/h # HTTP routes using registry
366 +│ ├── mcp-http-adapter.c/h # /mcp chunked JSON responses
367 +│ └── mcp-sse-adapter.c/h # /sse server-sent events
368 ├── schemas/
369 │ ├── execute_function.json # Static schema definitions
370 │ ├── query_metrics.json
@@ -383,25 +403,13 @@ src/web/mcp/
403
404 ## 7. Implementation Phases
405
386 -### Phase 1: Core Infrastructure (Priority: High)
387 -1. **MCP_REQ_JOB and response buffer structures**
388 -2. **Registry system with authorization**
389 -3. **Core execution function**
390 -4. **Basic HTTP adapter**
391 -
392 -### Phase 2: Transport Separation (Priority: High)
393 -1. **Extract JSON-RPC from WebSocket adapter**
394 -2. **Update all existing tools to use job interface**
395 -3. **Implement multi-buffer response system**
396 -4. **Complete HTTP adapter with full feature parity**
397 -
398 -### Phase 3: Advanced Features (Priority: Medium)
399 -1. **Specialized logs tools**
400 -2. **Enhanced error handling and status reporting**
406 +### Phase 1: Advanced Features (Priority: Medium)
407 +1. Specialized logs tools workflow.
408 +2. Enhanced error handling, status reporting, and potential job queue abstractions once multiple transports are stable.
409 3. **Performance optimizations**
410 4. **Comprehensive testing**
411
404 -### Phase 4: Future Enhancements (Priority: Low)
412 +### Phase 2: Future Enhancements (Priority: Low)
413 1. **Streaming support for long-running operations**
414 2. **Additional MCP namespaces (resources, prompts)**
415 3. **Advanced caching strategies**
@@ -415,4 +423,4 @@ src/web/mcp/
423 4. ✅ **Authorization**: Reuses existing HTTP_ACL/HTTP_ACCESS system
424 5. ✅ **Maintenance**: Single codebase for all MCP logic
425 6. ✅ **Performance**: No extra proxy/adapter process
418 -7. ✅ **Scalability**: Clean separation enables easy addition of new tools and transports
\ No newline at end of file
426 +7. ✅ **Scalability**: Clean separation enables easy addition of new tools and transports
src/web/mcp/adapters/mcp-http-common.h new
+51
@@ -0,0 +1,51 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_MCP_HTTP_COMMON_H
4 +#define NETDATA_MCP_HTTP_COMMON_H
5 +
6 +#include "web/server/web_client.h"
7 +
8 +#include <stdbool.h>
9 +#include <string.h>
10 +
11 +static inline bool mcp_http_extract_api_key(struct web_client *w, char *buffer, size_t buffer_len)
12 +{
13 + if (!w || !buffer || buffer_len == 0)
14 + return false;
15 +
16 + if (!w->url_query_string_decoded)
17 + return false;
18 +
19 + const char *query = buffer_tostring(w->url_query_string_decoded);
20 + if (!query || !*query)
21 + return false;
22 +
23 + if (*query == '?')
24 + query++;
25 +
26 + const char *api_key_str = strstr(query, "api_key=");
27 + if (!api_key_str)
28 + return false;
29 +
30 + api_key_str += strlen("api_key=");
31 +
32 + size_t i = 0;
33 + while (api_key_str[i] && api_key_str[i] != '&' && i < buffer_len - 1) {
34 + buffer[i] = api_key_str[i];
35 + i++;
36 + }
37 +
38 + buffer[i] = '\0';
39 + return i > 0;
40 +}
41 +
42 +static inline void mcp_http_disable_compression(struct web_client *w)
43 +{
44 + if (!w)
45 + return;
46 +
47 + web_client_flag_clear(w, WEB_CLIENT_CHUNKED_TRANSFER);
48 + w->response.zoutput = false;
49 +}
50 +
51 +#endif // NETDATA_MCP_HTTP_COMMON_H
src/web/mcp/adapters/mcp-http.c new
+214
@@ -0,0 +1,214 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "mcp-http.h"
4 +
5 +#include "web/server/web_client.h"
6 +#include "web/mcp/mcp-jsonrpc.h"
7 +#include "web/mcp/mcp.h"
8 +#include "web/mcp/adapters/mcp-sse.h"
9 +#include "mcp-http-common.h"
10 +
11 +#include "web/api/mcp_auth.h"
12 +
13 +#include "libnetdata/libnetdata.h"
14 +#include "libnetdata/http/http_defs.h"
15 +#include "libnetdata/http/content_type.h"
16 +
17 +#include <stdbool.h>
18 +#include <json-c/json.h>
19 +#include <string.h>
20 +#include <strings.h>
21 +
22 +#define IS_PARAM_SEPARATOR(c) ((c) == '&' || (c) == '\0')
23 +
24 +static const char *mcp_http_body(struct web_client *w, size_t *len) {
25 + if (!w || !w->payload)
26 + return NULL;
27 +
28 + const char *body = buffer_tostring(w->payload);
29 + if (!body)
30 + return NULL;
31 +
32 + if (len)
33 + *len = buffer_strlen(w->payload);
34 + return body;
35 +}
36 +
37 +static bool mcp_http_accepts_sse(struct web_client *w) {
38 + if (!w)
39 + return false;
40 +
41 + if (web_client_flag_check(w, WEB_CLIENT_FLAG_ACCEPT_SSE))
42 + return true;
43 +
44 + if (!w->url_query_string_decoded)
45 + return false;
46 +
47 + const char *qs = buffer_tostring(w->url_query_string_decoded);
48 + if (!qs || !*qs)
49 + return false;
50 +
51 + if (*qs == '?')
52 + qs++;
53 +
54 + if (!*qs)
55 + return false;
56 +
57 + const char *param = strstr(qs, "transport=");
58 + if (!param)
59 + return false;
60 +
61 + param += strlen("transport=");
62 + if (strncasecmp(param, "sse", 3) == 0 && IS_PARAM_SEPARATOR(param[3]))
63 + return true;
64 +
65 + return false;
66 +}
67 +
68 +#ifdef NETDATA_MCP_DEV_PREVIEW_API_KEY
69 +static void mcp_http_apply_api_key(struct web_client *w) {
70 + if (web_client_has_mcp_preview_key(w)) {
71 + web_client_set_permissions(w, HTTP_ACCESS_ALL, HTTP_USER_ROLE_ADMIN, USER_AUTH_METHOD_GOD);
72 + return;
73 + }
74 +
75 + char api_key_buffer[MCP_DEV_PREVIEW_API_KEY_LENGTH + 1];
76 + if (mcp_http_extract_api_key(w, api_key_buffer, sizeof(api_key_buffer)) &&
77 + mcp_api_key_verify(api_key_buffer)) {
78 + web_client_set_permissions(w, HTTP_ACCESS_ALL, HTTP_USER_ROLE_ADMIN, USER_AUTH_METHOD_GOD);
79 + }
80 +}
81 +#endif
82 +
83 +static void mcp_http_write_json_payload(struct web_client *w, BUFFER *payload) {
84 + if (!w)
85 + return;
86 +
87 + buffer_flush(w->response.data);
88 + w->response.data->content_type = CT_APPLICATION_JSON;
89 +
90 + if (payload && buffer_strlen(payload))
91 + buffer_fast_strcat(w->response.data, buffer_tostring(payload), buffer_strlen(payload));
92 +}
93 +
94 +static int mcp_http_prepare_error_response(struct web_client *w, BUFFER *payload, int http_code) {
95 + w->response.code = http_code;
96 + mcp_http_write_json_payload(w, payload);
97 + if (payload)
98 + buffer_free(payload);
99 + return http_code;
100 +}
101 +
102 +int mcp_http_handle_request(struct rrdhost *host __maybe_unused, struct web_client *w) {
103 + if (!w)
104 + return HTTP_RESP_INTERNAL_SERVER_ERROR;
105 +
106 + if (w->mode != HTTP_REQUEST_MODE_POST && w->mode != HTTP_REQUEST_MODE_GET) {
107 + buffer_flush(w->response.data);
108 + buffer_strcat(w->response.data, "Unsupported HTTP method for /mcp\n");
109 + w->response.data->content_type = CT_TEXT_PLAIN;
110 + w->response.code = HTTP_RESP_METHOD_NOT_ALLOWED;
111 + return w->response.code;
112 + }
113 +
114 +#ifdef NETDATA_MCP_DEV_PREVIEW_API_KEY
115 + mcp_http_apply_api_key(w);
116 +#endif
117 +
118 + size_t body_len = 0;
119 + const char *body = mcp_http_body(w, &body_len);
120 + if (!body || !body_len) {
121 + BUFFER *payload = mcp_jsonrpc_build_error_payload(NULL, -32600, "Empty request body", NULL, 0);
122 + return mcp_http_prepare_error_response(w, payload, HTTP_RESP_BAD_REQUEST);
123 + }
124 +
125 + enum json_tokener_error jerr = json_tokener_success;
126 + struct json_object *root = json_tokener_parse_verbose(body, &jerr);
127 + if (!root || jerr != json_tokener_success) {
128 + BUFFER *payload = mcp_jsonrpc_build_error_payload(NULL, -32700, json_tokener_error_desc(jerr), NULL, 0);
129 + if (root)
130 + json_object_put(root);
131 + return mcp_http_prepare_error_response(w, payload, HTTP_RESP_BAD_REQUEST);
132 + }
133 +
134 + MCP_CLIENT *mcpc = mcp_create_client(MCP_TRANSPORT_HTTP, w);
135 + if (!mcpc) {
136 + json_object_put(root);
137 + BUFFER *payload = mcp_jsonrpc_build_error_payload(NULL, -32603, "Failed to allocate MCP client", NULL, 0);
138 + return mcp_http_prepare_error_response(w, payload, HTTP_RESP_INTERNAL_SERVER_ERROR);
139 + }
140 + mcpc->user_auth = &w->user_auth;
141 +
142 + bool wants_sse = mcp_http_accepts_sse(w);
143 +
144 + int result_code = HTTP_RESP_INTERNAL_SERVER_ERROR;
145 +
146 + if (wants_sse) {
147 + mcpc->transport = MCP_TRANSPORT_SSE;
148 + mcpc->capabilities = MCP_CAPABILITY_ASYNC_COMMUNICATION |
149 + MCP_CAPABILITY_SUBSCRIPTIONS |
150 + MCP_CAPABILITY_NOTIFICATIONS;
151 + result_code = mcp_sse_serialize_response(w, mcpc, root);
152 + } else {
153 + BUFFER *response_payload = NULL;
154 + bool has_response = false;
155 +
156 + if (json_object_is_type(root, json_type_array)) {
157 + size_t len = json_object_array_length(root);
158 + BUFFER **responses = NULL;
159 + size_t responses_used = 0;
160 + size_t responses_size = 0;
161 +
162 + for (size_t i = 0; i < len; i++) {
163 + struct json_object *req_item = json_object_array_get_idx(root, i);
164 + BUFFER *resp_item = mcp_jsonrpc_process_single_request(mcpc, req_item, NULL);
165 + if (!resp_item)
166 + continue;
167 +
168 + if (responses_used == responses_size) {
169 + size_t new_size = responses_size ? responses_size * 2 : 4;
170 + BUFFER **tmp = reallocz(responses, new_size * sizeof(*tmp));
171 + if (!tmp) {
172 + buffer_free(resp_item);
173 + continue;
174 + }
175 + responses = tmp;
176 + responses_size = new_size;
177 + }
178 + responses[responses_used++] = resp_item;
179 + }
180 +
181 + if (responses_used) {
182 + response_payload = mcp_jsonrpc_build_batch_response(responses, responses_used);
183 + has_response = response_payload && buffer_strlen(response_payload);
184 + }
185 +
186 + for (size_t i = 0; i < responses_used; i++)
187 + buffer_free(responses[i]);
188 + freez(responses);
189 + } else {
190 + response_payload = mcp_jsonrpc_process_single_request(mcpc, root, NULL);
191 + has_response = response_payload && buffer_strlen(response_payload);
192 + }
193 +
194 + if (response_payload) {
195 + mcp_http_write_json_payload(w, response_payload);
196 + } else {
197 + buffer_flush(w->response.data);
198 + mcp_http_disable_compression(w);
199 + w->response.data->content_type = CT_APPLICATION_JSON;
200 + buffer_flush(w->response.header);
201 + }
202 +
203 + w->response.code = has_response ? HTTP_RESP_OK : HTTP_RESP_ACCEPTED;
204 +
205 + if (response_payload)
206 + buffer_free(response_payload);
207 +
208 + result_code = w->response.code;
209 + }
210 +
211 + json_object_put(root);
212 + mcp_free_client(mcpc);
213 + return result_code;
214 +}
src/web/mcp/adapters/mcp-http.h new
+11
@@ -0,0 +1,11 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_MCP_HTTP_ADAPTER_H
4 +#define NETDATA_MCP_HTTP_ADAPTER_H
5 +
6 +struct rrdhost;
7 +struct web_client;
8 +
9 +int mcp_http_handle_request(struct rrdhost *host, struct web_client *w);
10 +
11 +#endif // NETDATA_MCP_HTTP_ADAPTER_H
src/web/mcp/adapters/mcp-sse.c new
+201
@@ -0,0 +1,201 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "mcp-sse.h"
4 +
5 +#include "web/server/web_client.h"
6 +#include "web/mcp/mcp-jsonrpc.h"
7 +#include "web/mcp/mcp.h"
8 +#include "mcp-http-common.h"
9 +
10 +#include "web/api/mcp_auth.h"
11 +
12 +#include "libnetdata/libnetdata.h"
13 +#include "libnetdata/http/http_defs.h"
14 +#include "libnetdata/http/content_type.h"
15 +
16 +#include <json-c/json.h>
17 +
18 +static void mcp_sse_add_common_headers(struct web_client *w) {
19 + if (!w)
20 + return;
21 +
22 + buffer_flush(w->response.header);
23 + buffer_strcat(w->response.header, "Cache-Control: no-cache\r\n");
24 + buffer_strcat(w->response.header, "Connection: keep-alive\r\n");
25 +}
26 +
27 +#ifdef NETDATA_MCP_DEV_PREVIEW_API_KEY
28 +static void mcp_sse_apply_api_key(struct web_client *w) {
29 + if (web_client_has_mcp_preview_key(w)) {
30 + web_client_set_permissions(w, HTTP_ACCESS_ALL, HTTP_USER_ROLE_ADMIN, USER_AUTH_METHOD_GOD);
31 + return;
32 + }
33 +
34 + char api_key_buffer[MCP_DEV_PREVIEW_API_KEY_LENGTH + 1];
35 + if (mcp_http_extract_api_key(w, api_key_buffer, sizeof(api_key_buffer)) &&
36 + mcp_api_key_verify(api_key_buffer)) {
37 + web_client_set_permissions(w, HTTP_ACCESS_ALL, HTTP_USER_ROLE_ADMIN, USER_AUTH_METHOD_GOD);
38 + }
39 +}
40 +#endif
41 +
42 +static void mcp_sse_append_event(BUFFER *out, const char *event, const char *data) {
43 + if (!out || !event)
44 + return;
45 +
46 + buffer_strcat(out, "event: ");
47 + buffer_strcat(out, event);
48 + buffer_strcat(out, "\n");
49 +
50 + if (data && *data) {
51 + buffer_strcat(out, "data: ");
52 + buffer_strcat(out, data);
53 + buffer_strcat(out, "\n");
54 + }
55 +
56 + buffer_strcat(out, "\n");
57 +}
58 +
59 +static void mcp_sse_append_buffer_event(BUFFER *out, const char *event, BUFFER *payload) {
60 + if (!out || !event || !payload)
61 + return;
62 +
63 + buffer_strcat(out, "event: ");
64 + buffer_strcat(out, event);
65 + buffer_strcat(out, "\n");
66 +
67 + buffer_strcat(out, "data: ");
68 + buffer_fast_strcat(out, buffer_tostring(payload), buffer_strlen(payload));
69 + buffer_strcat(out, "\n\n");
70 +}
71 +
72 +int mcp_sse_serialize_response(struct web_client *w, MCP_CLIENT *mcpc, struct json_object *root) {
73 + if (!w || !mcpc || !root)
74 + return HTTP_RESP_INTERNAL_SERVER_ERROR;
75 +
76 + BUFFER **responses = NULL;
77 + size_t responses_used = 0;
78 + size_t responses_size = 0;
79 +
80 + if (json_object_is_type(root, json_type_array)) {
81 + size_t len = json_object_array_length(root);
82 + for (size_t i = 0; i < len; i++) {
83 + struct json_object *req_item = json_object_array_get_idx(root, i);
84 + BUFFER *resp_item = mcp_jsonrpc_process_single_request(mcpc, req_item, NULL);
85 + if (!resp_item)
86 + continue;
87 +
88 + if (responses_used == responses_size) {
89 + size_t new_size = responses_size ? responses_size * 2 : 4;
90 + BUFFER **tmp = reallocz(responses, new_size * sizeof(*tmp));
91 + if (!tmp) {
92 + buffer_free(resp_item);
93 + continue;
94 + }
95 + responses = tmp;
96 + responses_size = new_size;
97 + }
98 + responses[responses_used++] = resp_item;
99 + }
100 + } else {
101 + BUFFER *resp = mcp_jsonrpc_process_single_request(mcpc, root, NULL);
102 + if (resp) {
103 + responses = reallocz(responses, sizeof(*responses));
104 + if (responses)
105 + responses[responses_used++] = resp;
106 + else
107 + buffer_free(resp);
108 + }
109 + }
110 +
111 + buffer_flush(w->response.data);
112 + w->response.data->content_type = CT_TEXT_EVENT_STREAM;
113 + mcp_http_disable_compression(w);
114 + mcp_sse_add_common_headers(w);
115 +
116 + for (size_t i = 0; i < responses_used; i++) {
117 + if (!responses[i])
118 + continue;
119 + mcp_sse_append_buffer_event(w->response.data, "message", responses[i]);
120 + buffer_free(responses[i]);
121 + }
122 + freez(responses);
123 +
124 + mcp_sse_append_event(w->response.data, "complete", "{}");
125 +
126 + w->response.code = HTTP_RESP_OK;
127 + return w->response.code;
128 +}
129 +
130 +int mcp_sse_handle_request(struct rrdhost *host __maybe_unused, struct web_client *w) {
131 + if (!w)
132 + return HTTP_RESP_INTERNAL_SERVER_ERROR;
133 +
134 + if (w->mode != HTTP_REQUEST_MODE_GET && w->mode != HTTP_REQUEST_MODE_POST) {
135 + buffer_flush(w->response.data);
136 + buffer_strcat(w->response.data, "Unsupported HTTP method for /sse\n");
137 + w->response.data->content_type = CT_TEXT_PLAIN;
138 + w->response.code = HTTP_RESP_METHOD_NOT_ALLOWED;
139 + return w->response.code;
140 + }
141 +
142 +#ifdef NETDATA_MCP_DEV_PREVIEW_API_KEY
143 + mcp_sse_apply_api_key(w);
144 +#endif
145 +
146 + size_t body_len = 0;
147 + const char *body = NULL;
148 + if (w->payload)
149 + body = buffer_tostring(w->payload);
150 + if (body)
151 + body_len = buffer_strlen(w->payload);
152 +
153 + if (!body || !body_len) {
154 + buffer_flush(w->response.data);
155 + w->response.data->content_type = CT_TEXT_EVENT_STREAM;
156 + mcp_http_disable_compression(w);
157 + mcp_sse_add_common_headers(w);
158 + mcp_sse_append_event(w->response.data, "error", "Empty request body");
159 + w->response.code = HTTP_RESP_BAD_REQUEST;
160 + return w->response.code;
161 + }
162 +
163 + enum json_tokener_error jerr = json_tokener_success;
164 + struct json_object *root = json_tokener_parse_verbose(body, &jerr);
165 + if (!root || jerr != json_tokener_success) {
166 + BUFFER *payload = mcp_jsonrpc_build_error_payload(NULL, -32700, json_tokener_error_desc(jerr), NULL, 0);
167 + buffer_flush(w->response.data);
168 + w->response.data->content_type = CT_TEXT_EVENT_STREAM;
169 + mcp_http_disable_compression(w);
170 + mcp_sse_add_common_headers(w);
171 + if (payload) {
172 + mcp_sse_append_buffer_event(w->response.data, "error", payload);
173 + buffer_free(payload);
174 + } else {
175 + mcp_sse_append_event(w->response.data, "error", json_tokener_error_desc(jerr));
176 + }
177 + w->response.code = HTTP_RESP_BAD_REQUEST;
178 + if (root)
179 + json_object_put(root);
180 + return w->response.code;
181 + }
182 +
183 + MCP_CLIENT *mcpc = mcp_create_client(MCP_TRANSPORT_SSE, w);
184 + if (!mcpc) {
185 + json_object_put(root);
186 + buffer_flush(w->response.data);
187 + w->response.data->content_type = CT_TEXT_EVENT_STREAM;
188 + mcp_http_disable_compression(w);
189 + mcp_sse_add_common_headers(w);
190 + mcp_sse_append_event(w->response.data, "error", "Failed to allocate MCP client");
191 + w->response.code = HTTP_RESP_INTERNAL_SERVER_ERROR;
192 + return w->response.code;
193 + }
194 + mcpc->user_auth = &w->user_auth;
195 +
196 + int rc = mcp_sse_serialize_response(w, mcpc, root);
197 +
198 + json_object_put(root);
199 + mcp_free_client(mcpc);
200 + return rc;
201 +}
src/web/mcp/adapters/mcp-sse.h new
+16
@@ -0,0 +1,16 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_MCP_SSE_ADAPTER_H
4 +#define NETDATA_MCP_SSE_ADAPTER_H
5 +
6 +#include "web/mcp/mcp.h"
7 +
8 +struct rrdhost;
9 +struct web_client;
10 +struct json_object;
11 +
12 +int mcp_sse_handle_request(struct rrdhost *host, struct web_client *w);
13 +int mcp_sse_serialize_response(struct web_client *w, MCP_CLIENT *mcpc, struct json_object *root);
14 +
15 +
16 +#endif // NETDATA_MCP_SSE_ADAPTER_H
src/web/mcp/adapters/mcp-websocket.c
+74 -40
@@ -2,6 +2,9 @@
2
3 #include "mcp-websocket.h"
4 #include "web/websocket/websocket-internal.h"
5 +#include "web/mcp/mcp-jsonrpc.h"
6 +
7 +#include <string.h>
8
9 // Store the MCP context in the WebSocket client's data field
10 void mcp_websocket_set_context(struct websocket_server_client *wsc, MCP_CLIENT *ctx) {
@@ -15,19 +18,6 @@ MCP_CLIENT *mcp_websocket_get_context(struct websocket_server_client *wsc) {
18 return (MCP_CLIENT *)wsc->user_data;
19 }
20
18 -// WebSocket buffer sender function for the MCP adapter
19 -int mcp_websocket_send_buffer(struct websocket_server_client *wsc, BUFFER *buffer) {
20 - if (!wsc || !buffer) return -1;
21 -
22 - const char *text = buffer_tostring(buffer);
23 - if (!text || !*text) return -1;
24 -
25 - // Log the raw outgoing message
26 - netdata_log_debug(D_MCP, "SND: %s", text);
27 -
28 - return websocket_protocol_send_text(wsc, text);
29 -}
30 -
21 // Create a response context for a WebSocket client
22 static MCP_CLIENT *mcp_websocket_create_context(struct websocket_server_client *wsc) {
23 if (!wsc) return NULL;
@@ -56,6 +46,19 @@ void mcp_websocket_on_connect(struct websocket_server_client *wsc) {
46 websocket_debug(wsc, "MCP client connected");
47 }
48
49 +static void mcp_websocket_send_payload(struct websocket_server_client *wsc, BUFFER *payload) {
50 + if (!wsc || !payload)
51 + return;
52 +
53 + const char *text = buffer_tostring(payload);
54 + if (!text)
55 + return;
56 +
57 + netdata_log_debug(D_MCP, "SND: %s", text);
58 + websocket_protocol_send_text(wsc, text);
59 +}
60 +
61 +
62 // WebSocket message handler for MCP - receives message and routes to MCP
63 void mcp_websocket_on_message(struct websocket_server_client *wsc, const char *message, size_t length, WEBSOCKET_OPCODE opcode) {
64 if (!wsc || !message || length == 0)
@@ -89,37 +92,68 @@ void mcp_websocket_on_message(struct websocket_server_client *wsc, const char *m
92 request = json_tokener_parse_verbose(message, &jerr);
93
94 if (!request || jerr != json_tokener_success) {
92 - // Log the full error with payload for debugging
93 - websocket_error(wsc, "Failed to parse JSON-RPC request: %s | Payload (length=%zu): '%.*s'",
94 - json_tokener_error_desc(jerr),
95 - length,
96 - (int)(length > 1000 ? 1000 : length), // Limit to 1000 chars in log
97 - message);
98 -
99 - // Also log the hex dump of first few bytes to catch non-printable characters
100 - if (length > 0) {
101 - char hex_dump[256];
102 - size_t hex_len = 0;
103 - size_t bytes_to_dump = (length > 32) ? 32 : length;
104 -
105 - for (size_t i = 0; i < bytes_to_dump && hex_len < sizeof(hex_dump) - 6; i++) {
106 - hex_len += snprintf(hex_dump + hex_len, sizeof(hex_dump) - hex_len,
107 - "%02X ", (unsigned char)message[i]);
95 + websocket_error(wsc, "Failed to parse JSON-RPC request: %s", json_tokener_error_desc(jerr));
96 +
97 + BUFFER *error_payload = mcp_jsonrpc_build_error_payload(NULL, -32700, "Parse error", NULL, 0);
98 + mcp_websocket_send_payload(wsc, error_payload);
99 + buffer_free(error_payload);
100 + return;
101 + }
102 +
103 + if (json_object_is_type(request, json_type_array)) {
104 + int len = (int)json_object_array_length(request);
105 + BUFFER **responses = NULL;
106 + size_t responses_used = 0;
107 + size_t responses_size = 0;
108 +
109 + for (int i = 0; i < len; i++) {
110 + struct json_object *req_item = json_object_array_get_idx(request, i);
111 + BUFFER *resp_item = mcp_jsonrpc_process_single_request(mcpc, req_item, NULL);
112 + if (resp_item) {
113 + if (responses_used == responses_size) {
114 + size_t new_size = responses_size ? responses_size * 2 : 4;
115 + BUFFER **tmp = reallocz(responses, new_size * sizeof(*tmp));
116 + if (!tmp) {
117 + buffer_free(resp_item);
118 + continue;
119 + }
120 + responses = tmp;
121 + responses_size = new_size;
122 + }
123 + responses[responses_used++] = resp_item;
124 }
109 - if (bytes_to_dump < length) {
110 - hex_len += snprintf(hex_dump + hex_len, sizeof(hex_dump) - hex_len, "...");
125 + }
126 +
127 + if (responses_used > 0) {
128 + size_t total_len = 2; // brackets
129 + for (size_t i = 0; i < responses_used; i++)
130 + total_len += buffer_strlen(responses[i]) + (i ? 1 : 0);
131 +
132 + BUFFER *batch = buffer_create(total_len + 32, NULL);
133 + buffer_fast_strcat(batch, "[", 1);
134 + for (size_t i = 0; i < responses_used; i++) {
135 + if (i)
136 + buffer_fast_strcat(batch, ",", 1);
137 + const char *resp_text = buffer_tostring(responses[i]);
138 + size_t resp_len = buffer_strlen(responses[i]);
139 + buffer_fast_strcat(batch, resp_text, resp_len);
140 }
112 -
113 - websocket_error(wsc, "First %zu bytes hex dump: %s", bytes_to_dump, hex_dump);
141 + buffer_fast_strcat(batch, "]", 1);
142 + mcp_websocket_send_payload(wsc, batch);
143 + buffer_free(batch);
144 + }
145 +
146 + for (size_t i = 0; i < responses_used; i++)
147 + buffer_free(responses[i]);
148 + freez(responses);
149 + } else {
150 + BUFFER *response = mcp_jsonrpc_process_single_request(mcpc, request, NULL);
151 + if (response) {
152 + mcp_websocket_send_payload(wsc, response);
153 + buffer_free(response);
154 }
115 -
116 - return;
155 }
118 -
119 - // Pass the request to the MCP handler
120 - mcp_handle_request(mcpc, request);
121 -
122 - // Free the request object
156 +
157 json_object_put(request);
158 }
159
src/web/mcp/adapters/mcp-websocket.h
+1 -5
@@ -15,12 +15,8 @@ void mcp_websocket_on_message(struct websocket_server_client *wsc, const char *m
15 void mcp_websocket_on_close(struct websocket_server_client *wsc, WEBSOCKET_CLOSE_CODE code, const char *reason);
16 void mcp_websocket_on_disconnect(struct websocket_server_client *wsc);
17
18 -// Helper functions for the WebSocket adapter
19 -int mcp_websocket_send_json(struct websocket_server_client *wsc, struct json_object *json);
20 -int mcp_websocket_send_buffer(struct websocket_server_client *wsc, BUFFER *buffer);
21 -
18 // Get and set MCP context from a WebSocket client
19 MCP_CLIENT *mcp_websocket_get_context(struct websocket_server_client *wsc);
20 void mcp_websocket_set_context(struct websocket_server_client *wsc, MCP_CLIENT *ctx);
21
26 -#endif // NETDATA_MCP_ADAPTER_WEBSOCKET_H
\ No newline at end of file
22 +#endif // NETDATA_MCP_ADAPTER_WEBSOCKET_H
src/web/mcp/bridges/stdio-golang/nd-mcp.go
+44 -4
@@ -13,6 +13,7 @@ import (
13 "net/http"
14 "os"
15 "os/signal"
16 + "strings"
17 "sync"
18 "syscall"
19 "time"
@@ -74,11 +75,47 @@ func main() {
75 programName = os.Args[0]
76 }
77
77 - if len(os.Args) != 2 {
78 - fmt.Fprintf(os.Stderr, "%s: Usage: %s ws://host/path\n", programName, programName)
78 + args := os.Args[1:]
79 + var targetURL string
80 + var bearerToken string
81 +
82 + for len(args) > 0 {
83 + arg := args[0]
84 + switch {
85 + case arg == "--bearer":
86 + if len(args) < 2 {
87 + fmt.Fprintf(os.Stderr, "%s: Usage: %s [--bearer TOKEN] ws://host/path\n", programName, programName)
88 + os.Exit(1)
89 + }
90 + bearerToken = strings.TrimSpace(args[1])
91 + args = args[2:]
92 + case strings.HasPrefix(arg, "--bearer="):
93 + bearerToken = strings.TrimSpace(strings.TrimPrefix(arg, "--bearer="))
94 + args = args[1:]
95 + default:
96 + if targetURL != "" {
97 + fmt.Fprintf(os.Stderr, "%s: Unexpected argument '%s'\n", programName, arg)
98 + fmt.Fprintf(os.Stderr, "%s: Usage: %s [--bearer TOKEN] ws://host/path\n", programName, programName)
99 + os.Exit(1)
100 + }
101 + targetURL = arg
102 + args = args[1:]
103 + }
104 + }
105 +
106 + if targetURL == "" {
107 + fmt.Fprintf(os.Stderr, "%s: Usage: %s [--bearer TOKEN] ws://host/path\n", programName, programName)
108 os.Exit(1)
109 }
110
111 + if bearerToken == "" {
112 + bearerToken = strings.TrimSpace(os.Getenv("ND_MCP_BEARER_TOKEN"))
113 + }
114 +
115 + if bearerToken != "" {
116 + fmt.Fprintf(os.Stderr, "%s: Authorization header enabled for MCP connection\n", programName)
117 + }
118 +
119 // Set up channels for communication
120 stdinCh := make(chan string, 100) // Buffer stdin messages
121 reconnectCh := make(chan struct{}, 1) // Signal for immediate reconnection
@@ -335,15 +372,18 @@ func main() {
372 connectionCtx, connectionCancel := context.WithTimeout(ctx, 15*time.Second)
373 defer connectionCancel()
374
338 - fmt.Fprintf(os.Stderr, "%s: Connecting to %s...\n", programName, os.Args[1])
375 + fmt.Fprintf(os.Stderr, "%s: Connecting to %s...\n", programName, targetURL)
376
377 // Create a custom header with the WebSocket key
378 header := http.Header{}
379 header.Set("Sec-WebSocket-Key", generateWebSocketKey())
380 header.Set("Sec-WebSocket-Version", "13")
381 + if bearerToken != "" {
382 + header.Set("Authorization", "Bearer "+bearerToken)
383 + }
384
385 // Connect to WebSocket
346 - conn, _, err := websocket.Dial(connectionCtx, os.Args[1], &websocket.DialOptions{
386 + conn, _, err := websocket.Dial(connectionCtx, targetURL, &websocket.DialOptions{
387 CompressionMode: websocket.CompressionContextTakeover,
388 HTTPHeader: header,
389 })
src/web/mcp/bridges/stdio-nodejs/nd-mcp.js
+45 -5
@@ -6,11 +6,45 @@ const path = require('path');
6 // Get program name for logs
7 const PROGRAM_NAME = path.basename(process.argv[1] || 'nd-mcp-nodejs');
8
9 -if (process.argv.length !== 3) {
10 - console.error(`${PROGRAM_NAME}: Usage: ${PROGRAM_NAME} ws://host/path`);
9 +function usage() {
10 + console.error(`${PROGRAM_NAME}: Usage: ${PROGRAM_NAME} [--bearer TOKEN] ws://host/path`);
11 process.exit(1);
12 }
13
14 +const parsedArgs = process.argv.slice(2);
15 +let targetURL = '';
16 +let bearerToken = '';
17 +
18 +for (let i = 0; i < parsedArgs.length;) {
19 + const arg = parsedArgs[i];
20 +
21 + if (arg === '--bearer') {
22 + if (i + 1 >= parsedArgs.length) usage();
23 + bearerToken = parsedArgs[i + 1].trim();
24 + i += 2;
25 + }
26 + else if (arg.startsWith('--bearer=')) {
27 + bearerToken = arg.substring('--bearer='.length).trim();
28 + i += 1;
29 + }
30 + else {
31 + if (targetURL) usage();
32 + targetURL = arg;
33 + i += 1;
34 + }
35 +}
36 +
37 +if (!targetURL) usage();
38 +
39 +if (!bearerToken) {
40 + const envToken = process.env.ND_MCP_BEARER_TOKEN;
41 + if (envToken) bearerToken = envToken.trim();
42 +}
43 +
44 +if (bearerToken) {
45 + console.error(`${PROGRAM_NAME}: Authorization header enabled for MCP connection`);
46 +}
47 +
48 // Reconnection settings
49 const MAX_RECONNECT_DELAY_MS = 60000; // 60 seconds
50 const BASE_DELAY_MS = 1000; // 1 second
@@ -209,7 +243,7 @@ function attemptConnection() {
243 }
244
245 connectingInProgress = true;
212 - console.error(`${PROGRAM_NAME}: Connecting to ${process.argv[2]}...`);
246 + console.error(`${PROGRAM_NAME}: Connecting to ${targetURL}...`);
247
248 // Close any existing websocket
249 if (ws) {
@@ -231,7 +265,13 @@ function attemptConnection() {
265 pingTimeout: 10000 // 10 seconds to wait for pong
266 };
267
234 - ws = new WebSocket(process.argv[2], wsOptions);
268 + if (bearerToken) {
269 + wsOptions.headers = {
270 + Authorization: `Bearer ${bearerToken}`
271 + };
272 + }
273 +
274 + ws = new WebSocket(targetURL, wsOptions);
275
276 // Set a timeout for initial connection
277 const connectionTimeout = setTimeout(() => {
@@ -382,4 +422,4 @@ process.on('SIGTERM', () => {
422 });
423
424 // Start the connection process
385 -connect();
\ No newline at end of file
425 +connect();
src/web/mcp/bridges/stdio-python/nd-mcp.py
+62 -15
@@ -4,7 +4,7 @@
4 import sys
5 import asyncio
6 import websockets
7 -import os.path
7 +import os
8 import random
9 import time
10 import signal
@@ -43,7 +43,7 @@ def create_jsonrpc_error(id, code, message, data=None):
43 response["error"]["data"] = data
44 return json.dumps(response)
45
46 -async def connect_with_backoff(uri):
46 +async def connect_with_backoff(uri, bearer_token):
47 max_delay = 60 # Maximum delay between reconnections in seconds
48 base_delay = 1 # Initial delay in seconds
49 retry_count = 0
@@ -168,18 +168,27 @@ async def connect_with_backoff(uri):
168 pass
169
170 print(f"{PROGRAM_NAME}: Connecting to {uri}...", file=sys.stderr)
171 -
171 +
172 try:
173 # Connect with timeout
174 # In newer versions of websockets, connect() is already awaitable
175 + connect_kwargs = {
176 + "compression": 'deflate',
177 + "max_size": 16*1024*1024,
178 + "ping_interval": 30,
179 + "ping_timeout": 10,
180 + "close_timeout": 5
181 + }
182 +
183 + if bearer_token:
184 + connect_kwargs["extra_headers"] = {
185 + "Authorization": f"Bearer {bearer_token}"
186 + }
187 +
188 ws = await asyncio.wait_for(
189 websockets.connect(
177 - uri,
178 - compression='deflate',
179 - max_size=16*1024*1024,
180 - ping_interval=30, # Send keep-alive pings every 30 seconds
181 - ping_timeout=10, # Wait 10 seconds for pong response
182 - close_timeout=5 # Wait 5 seconds for close frame
190 + uri,
191 + **connect_kwargs
192 ),
193 timeout=15 # 15 second timeout
194 )
@@ -324,11 +333,49 @@ async def connect_with_backoff(uri):
333 print(f"{PROGRAM_NAME}: Unexpected error: {e}", file=sys.stderr)
334 retry_count += 1
335
336 +def usage():
337 + print(f"{PROGRAM_NAME}: Usage: {PROGRAM_NAME} [--bearer TOKEN] ws://host/path", file=sys.stderr)
338 + sys.exit(1)
339 +
340 +
341 +def parse_args(argv):
342 + target = None
343 + bearer = None
344 + idx = 0
345 +
346 + while idx < len(argv):
347 + arg = argv[idx]
348 + if arg == '--bearer':
349 + if idx + 1 >= len(argv):
350 + usage()
351 + bearer = argv[idx + 1].strip()
352 + idx += 2
353 + elif arg.startswith('--bearer='):
354 + bearer = arg.split('=', 1)[1].strip()
355 + idx += 1
356 + else:
357 + if target is not None:
358 + usage()
359 + target = arg
360 + idx += 1
361 +
362 + if not target:
363 + usage()
364 +
365 + return target, bearer
366 +
367 +
368 def main():
328 - if len(sys.argv) != 2:
329 - print(f"{PROGRAM_NAME}: Usage: {PROGRAM_NAME} ws://host/path", file=sys.stderr)
330 - sys.exit(1)
331 -
369 + target_uri, bearer_token = parse_args(sys.argv[1:])
370 +
371 + if not bearer_token:
372 + env_token = os.environ.get("ND_MCP_BEARER_TOKEN", "")
373 + if env_token:
374 + bearer_token = env_token.strip()
375 +
376 + if bearer_token:
377 + print(f"{PROGRAM_NAME}: Authorization header enabled for MCP connection", file=sys.stderr)
378 +
379 # Set up signal handling
380 def signal_handler(sig, frame):
381 print(f"{PROGRAM_NAME}: Received signal {sig}, exiting", file=sys.stderr)
@@ -338,7 +385,7 @@ def main():
385 signal.signal(signal.SIGTERM, signal_handler)
386
387 try:
341 - asyncio.run(connect_with_backoff(sys.argv[1]))
388 + asyncio.run(connect_with_backoff(target_uri, bearer_token))
389 except KeyboardInterrupt:
390 print(f"{PROGRAM_NAME}: Interrupted by user, exiting", file=sys.stderr)
391
@@ -346,4 +393,4 @@ def main():
393 print(f"{PROGRAM_NAME}: Exiting due to stdin error", file=sys.stderr)
394
395 if __name__ == "__main__":
349 - main()
\ No newline at end of file
396 + main()
src/web/mcp/mcp-completion.c
+5 -9
@@ -28,8 +28,8 @@
28 #include "mcp-completion.h"
29
30 // Implementation of completion/complete (transport-agnostic)
31 -static MCP_RETURN_CODE mcp_completion_method_complete(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) {
32 - if (!mcpc || id == 0) return MCP_RC_ERROR;
31 +static MCP_RETURN_CODE mcp_completion_method_complete(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id __maybe_unused) {
32 + if (!mcpc) return MCP_RC_ERROR;
33
34 // Extract argument and ref parameters
35 struct json_object *argument_obj = NULL;
@@ -91,13 +91,9 @@ static MCP_RETURN_CODE mcp_completion_method_complete(MCP_CLIENT *mcpc, struct j
91 // Completion namespace method dispatcher (transport-agnostic)
92 MCP_RETURN_CODE mcp_completion_route(MCP_CLIENT *mcpc, const char *method, struct json_object *params, MCP_REQUEST_ID id) {
93 if (!mcpc || !method) return MCP_RC_INTERNAL_ERROR;
94 -
94 +
95 netdata_log_debug(D_MCP, "MCP completion method: %s", method);
96 -
97 - // Flush previous buffers
98 - buffer_flush(mcpc->result);
99 - buffer_flush(mcpc->error);
100 -
96 +
97 MCP_RETURN_CODE rc;
98
99 if (strcmp(method, "complete") == 0) {
@@ -110,4 +106,4 @@ MCP_RETURN_CODE mcp_completion_route(MCP_CLIENT *mcpc, const char *method, struc
106 }
107
108 return rc;
113 -}
\ No newline at end of file
109 +}
src/web/mcp/mcp-jsonrpc.c new
+209
@@ -0,0 +1,209 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#include "mcp-jsonrpc.h"
4 +
5 +#include <string.h>
6 +
7 +static const size_t MCP_JSONRPC_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
8 +
9 +static void buffer_append_json_id(BUFFER *out, struct json_object *id_obj) {
10 + if (!id_obj) {
11 + buffer_strcat(out, "null");
12 + return;
13 + }
14 +
15 + const char *id_text = json_object_to_json_string_ext(id_obj, JSON_C_TO_STRING_PLAIN);
16 + if (!id_text)
17 + id_text = "null";
18 + buffer_fast_strcat(out, id_text, strlen(id_text));
19 +}
20 +
21 +static void buffer_append_json_string_value(BUFFER *out, const char *text) {
22 + struct json_object *tmp = json_object_new_string(text ? text : "");
23 + const char *payload = json_object_to_json_string_ext(tmp, JSON_C_TO_STRING_PLAIN);
24 + if (payload)
25 + buffer_fast_strcat(out, payload, strlen(payload));
26 + json_object_put(tmp);
27 +}
28 +
29 +int mcp_jsonrpc_error_code(MCP_RETURN_CODE rc) {
30 + switch (rc) {
31 + case MCP_RC_INVALID_PARAMS:
32 + return -32602;
33 + case MCP_RC_NOT_FOUND:
34 + case MCP_RC_NOT_IMPLEMENTED:
35 + return -32601;
36 + case MCP_RC_BAD_REQUEST:
37 + return -32600;
38 + case MCP_RC_INTERNAL_ERROR:
39 + return -32603;
40 + case MCP_RC_OK:
41 + return 0;
42 + case MCP_RC_ERROR:
43 + default:
44 + return -32000;
45 + }
46 +}
47 +
48 +BUFFER *mcp_jsonrpc_build_error_payload(struct json_object *id_obj, int code, const char *message,
49 + const struct mcp_response_chunk *chunks, size_t chunk_count) {
50 + BUFFER *out = buffer_create(512, NULL);
51 + buffer_strcat(out, "{\"jsonrpc\":\"2.0\",\"id\":");
52 + buffer_append_json_id(out, id_obj);
53 + buffer_strcat(out, ",\"error\":{\"code\":");
54 + buffer_sprintf(out, "%d", code);
55 + buffer_strcat(out, ",\"message\":");
56 + buffer_append_json_string_value(out, message ? message : "");
57 +
58 + if (chunk_count >= 1 && chunks && chunks[0].buffer && buffer_strlen(chunks[0].buffer)) {
59 + buffer_strcat(out, ",\"data\":");
60 + if (chunks[0].type == MCP_RESPONSE_CHUNK_JSON)
61 + buffer_fast_strcat(out, buffer_tostring(chunks[0].buffer), buffer_strlen(chunks[0].buffer));
62 + else
63 + buffer_append_json_string_value(out, buffer_tostring(chunks[0].buffer));
64 + }
65 +
66 + buffer_strcat(out, "}}");
67 + return out;
68 +}
69 +
70 +BUFFER *mcp_jsonrpc_build_success_payload(struct json_object *id_obj, const struct mcp_response_chunk *chunk) {
71 + const char *chunk_text = chunk && chunk->buffer ? buffer_tostring(chunk->buffer) : NULL;
72 + size_t chunk_len = chunk_text ? buffer_strlen(chunk->buffer) : 0;
73 +
74 + BUFFER *out = buffer_create(64 + chunk_len, NULL);
75 + buffer_strcat(out, "{\"jsonrpc\":\"2.0\",\"id\":");
76 + buffer_append_json_id(out, id_obj);
77 + buffer_strcat(out, ",\"result\":");
78 + if (chunk_text && chunk_len)
79 + buffer_fast_strcat(out, chunk_text, chunk_len);
80 + else
81 + buffer_strcat(out, "{}");
82 + buffer_strcat(out, "}");
83 + return out;
84 +}
85 +
86 +BUFFER *mcp_jsonrpc_process_single_request(MCP_CLIENT *mcpc, struct json_object *request, bool *had_error) {
87 + if (had_error)
88 + *had_error = false;
89 +
90 + if (!mcpc || !request)
91 + return NULL;
92 +
93 + struct json_object *id_obj = NULL;
94 + bool has_id = json_object_is_type(request, json_type_object) && json_object_object_get_ex(request, "id", &id_obj);
95 +
96 + if (!json_object_is_type(request, json_type_object))
97 + return mcp_jsonrpc_build_error_payload(has_id ? id_obj : NULL, -32600, "Invalid request", NULL, 0);
98 +
99 + struct json_object *jsonrpc_obj = NULL;
100 + if (!json_object_object_get_ex(request, "jsonrpc", &jsonrpc_obj) ||
101 + !json_object_is_type(jsonrpc_obj, json_type_string) ||
102 + strcmp(json_object_get_string(jsonrpc_obj), "2.0") != 0) {
103 + return mcp_jsonrpc_build_error_payload(has_id ? id_obj : NULL, -32600, "Invalid or missing jsonrpc version", NULL, 0);
104 + }
105 +
106 + struct json_object *method_obj = NULL;
107 + if (!json_object_object_get_ex(request, "method", &method_obj) ||
108 + !json_object_is_type(method_obj, json_type_string)) {
109 + return mcp_jsonrpc_build_error_payload(has_id ? id_obj : NULL, -32600, "Missing or invalid method", NULL, 0);
110 + }
111 + const char *method = json_object_get_string(method_obj);
112 +
113 + struct json_object *params_obj = NULL;
114 + bool params_created = false;
115 + if (json_object_object_get_ex(request, "params", &params_obj)) {
116 + if (!json_object_is_type(params_obj, json_type_object)) {
117 + return mcp_jsonrpc_build_error_payload(has_id ? id_obj : NULL, -32602, "Params must be an object", NULL, 0);
118 + }
119 + } else {
120 + params_obj = json_object_new_object();
121 + params_created = true;
122 + }
123 +
124 + MCP_RETURN_CODE rc = mcp_dispatch_method(mcpc, method, params_obj, has_id ? 1 : 0);
125 +
126 + if (params_created)
127 + json_object_put(params_obj);
128 +
129 + size_t total_bytes = mcp_client_response_size(mcpc);
130 + if (total_bytes > MCP_JSONRPC_RESPONSE_MAX_BYTES) {
131 + BUFFER *payload = mcp_jsonrpc_build_error_payload(has_id ? id_obj : NULL,
132 + -32001,
133 + "Response too large for transport",
134 + NULL, 0);
135 + mcp_client_release_response(mcpc);
136 + mcp_client_clear_error(mcpc);
137 + if (had_error)
138 + *had_error = true;
139 + return payload;
140 + }
141 +
142 + if (!has_id) {
143 + mcp_client_release_response(mcpc);
144 + mcp_client_clear_error(mcpc);
145 + return NULL;
146 + }
147 +
148 + const struct mcp_response_chunk *chunks = mcp_client_response_chunks(mcpc);
149 + size_t chunk_count = mcp_client_response_chunk_count(mcpc);
150 +
151 + BUFFER *payload = NULL;
152 +
153 + if (rc == MCP_RC_OK && !mcpc->last_response_error) {
154 + if (!chunks || chunk_count == 0) {
155 + payload = mcp_jsonrpc_build_error_payload(id_obj, -32603, "Empty response", NULL, 0);
156 + if (had_error)
157 + *had_error = true;
158 + }
159 + else if (chunk_count > 1 || chunks[0].type != MCP_RESPONSE_CHUNK_JSON) {
160 + payload = mcp_jsonrpc_build_error_payload(id_obj, -32002, "Streaming responses not supported on this transport", NULL, 0);
161 + if (had_error)
162 + *had_error = true;
163 + }
164 + else {
165 + payload = mcp_jsonrpc_build_success_payload(id_obj, &chunks[0]);
166 + }
167 + } else {
168 + const char *message = mcp_client_error_message(mcpc);
169 + if (!message)
170 + message = MCP_RETURN_CODE_2str(rc);
171 + payload = mcp_jsonrpc_build_error_payload(id_obj, mcp_jsonrpc_error_code(rc), message, chunks, chunk_count);
172 + if (had_error)
173 + *had_error = true;
174 + }
175 +
176 + mcp_client_release_response(mcpc);
177 + mcp_client_clear_error(mcpc);
178 + return payload;
179 +}
180 +
181 +BUFFER *mcp_jsonrpc_build_batch_response(BUFFER **responses, size_t count) {
182 + if (!responses || count == 0)
183 + return NULL;
184 +
185 + size_t total_len = 2; // []
186 + for (size_t i = 0; i < count; i++) {
187 + if (!responses[i])
188 + continue;
189 + total_len += buffer_strlen(responses[i]);
190 + if (i)
191 + total_len += 1;
192 + }
193 +
194 + BUFFER *batch = buffer_create(total_len + 32, NULL);
195 + buffer_strcat(batch, "[");
196 + bool first = true;
197 + for (size_t i = 0; i < count; i++) {
198 + if (!responses[i])
199 + continue;
200 + if (!first)
201 + buffer_strcat(batch, ",");
202 + first = false;
203 + const char *resp_text = buffer_tostring(responses[i]);
204 + size_t resp_len = buffer_strlen(responses[i]);
205 + buffer_fast_strcat(batch, resp_text, resp_len);
206 + }
207 + buffer_strcat(batch, "]");
208 + return batch;
209 +}
src/web/mcp/mcp-jsonrpc.h new
+16
@@ -0,0 +1,16 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +#ifndef NETDATA_MCP_JSONRPC_H
4 +#define NETDATA_MCP_JSONRPC_H
5 +
6 +#include <json-c/json.h>
7 +#include "mcp.h"
8 +
9 +int mcp_jsonrpc_error_code(MCP_RETURN_CODE rc);
10 +BUFFER *mcp_jsonrpc_build_error_payload(struct json_object *id_obj, int code, const char *message,
11 + const struct mcp_response_chunk *chunks, size_t chunk_count);
12 +BUFFER *mcp_jsonrpc_build_success_payload(struct json_object *id_obj, const struct mcp_response_chunk *chunk);
13 +BUFFER *mcp_jsonrpc_process_single_request(MCP_CLIENT *mcpc, struct json_object *request, bool *had_error);
14 +BUFFER *mcp_jsonrpc_build_batch_response(BUFFER **responses, size_t count);
15 +
16 +#endif // NETDATA_MCP_JSONRPC_H
src/web/mcp/mcp-logging.c
+5 -9
@@ -28,8 +28,8 @@
28 #include "mcp-logging.h"
29
30 // Implementation of logging/setLevel (transport-agnostic)
31 -static MCP_RETURN_CODE mcp_logging_method_setLevel(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) {
32 - if (!mcpc || id == 0)
31 +static MCP_RETURN_CODE mcp_logging_method_setLevel(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id __maybe_unused) {
32 + if (!mcpc)
33 return MCP_RC_ERROR;
34
35 // Extract level parameter
@@ -75,13 +75,9 @@ static MCP_RETURN_CODE mcp_logging_method_setLevel(MCP_CLIENT *mcpc, struct json
75 // Logging namespace method dispatcher (transport-agnostic)
76 MCP_RETURN_CODE mcp_logging_route(MCP_CLIENT *mcpc, const char *method, struct json_object *params, MCP_REQUEST_ID id) {
77 if (!mcpc || !method) return MCP_RC_INTERNAL_ERROR;
78 -
78 +
79 netdata_log_debug(D_MCP, "MCP logging method: %s", method);
80 -
81 - // Flush previous buffers
82 - buffer_flush(mcpc->result);
83 - buffer_flush(mcpc->error);
84 -
80 +
81 MCP_RETURN_CODE rc;
82
83 if (strcmp(method, "setLevel") == 0) {
@@ -94,4 +90,4 @@ MCP_RETURN_CODE mcp_logging_route(MCP_CLIENT *mcpc, const char *method, struct j
90 }
91
92 return rc;
97 -}
\ No newline at end of file
93 +}
src/web/mcp/mcp-prompts.c
+4 -8
@@ -39,8 +39,8 @@
39 #include "mcp-prompts.h"
40
41 // Implementation of prompts/list (transport-agnostic)
42 -static MCP_RETURN_CODE mcp_prompts_method_list(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, MCP_REQUEST_ID id) {
43 - if (!mcpc || id == 0)
42 +static MCP_RETURN_CODE mcp_prompts_method_list(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, MCP_REQUEST_ID id __maybe_unused) {
43 + if (!mcpc)
44 return MCP_RC_ERROR;
45
46 // Initialize success response
@@ -70,13 +70,9 @@ static MCP_RETURN_CODE mcp_prompts_method_get(MCP_CLIENT *mcpc, struct json_obje
70 // Prompts namespace method dispatcher (transport-agnostic)
71 MCP_RETURN_CODE mcp_prompts_route(MCP_CLIENT *mcpc, const char *method, struct json_object *params, MCP_REQUEST_ID id) {
72 if (!mcpc || !method) return MCP_RC_INTERNAL_ERROR;
73 -
73 +
74 netdata_log_debug(D_MCP, "MCP prompts method: %s", method);
75 -
76 - // Flush previous buffers
77 - buffer_flush(mcpc->result);
78 - buffer_flush(mcpc->error);
79 -
75 +
76 MCP_RETURN_CODE rc;
77
78 if (strcmp(method, "list") == 0) {
src/web/mcp/mcp-request-id.c deleted
-174
@@ -1,174 +0,0 @@
1 -// SPDX-License-Identifier: GPL-3.0-or-later
2 -
3 -#include "mcp-request-id.h"
4 -#include "mcp.h"
5 -
6 -// Request ID structure - stored in JudyL array
7 -typedef struct mcp_request_id_entry {
8 - enum {
9 - MCP_REQUEST_ID_TYPE_INT,
10 - MCP_REQUEST_ID_TYPE_STRING
11 - } type;
12 -
13 - union {
14 - int64_t int_value;
15 - STRING *str_value;
16 - };
17 -} MCP_REQUEST_ID_ENTRY;
18 -
19 -/**
20 - * Extract and register a request ID from a JSON object
21 - *
22 - * @param mcpc The MCP client context
23 - * @param request The JSON request object that may contain an ID
24 - * @return MCP_REQUEST_ID - the assigned ID (0 if no ID was present)
25 - */
26 -MCP_REQUEST_ID mcp_request_id_add(MCP_CLIENT *mcpc, struct json_object *request) {
27 - if (!mcpc || !request)
28 - return 0;
29 -
30 - // Extract ID (optional, for notifications)
31 - struct json_object *id_obj = NULL;
32 - bool has_id = json_object_object_get_ex(request, "id", &id_obj);
33 -
34 - if (!has_id)
35 - return 0;
36 -
37 - // Allocate a new entry
38 - MCP_REQUEST_ID_ENTRY *entry = callocz(1, sizeof(MCP_REQUEST_ID_ENTRY));
39 -
40 - // Generate a new sequential ID
41 - MCP_REQUEST_ID id = ++mcpc->request_id_counter;
42 -
43 - // Store the entry in the JudyL array
44 - Word_t Index = (Word_t)id;
45 - Pvoid_t *PValue = JudyLIns(&mcpc->request_ids, Index, NULL);
46 - if (unlikely(PValue == PJERR)) {
47 - netdata_log_error("MCP: JudyLIns failed for request ID %zu", id);
48 - freez(entry);
49 - return 0;
50 - }
51 -
52 - // Parse the ID value
53 - if (json_object_get_type(id_obj) == json_type_int) {
54 - entry->type = MCP_REQUEST_ID_TYPE_INT;
55 - entry->int_value = json_object_get_int64(id_obj);
56 - }
57 - else if (json_object_get_type(id_obj) == json_type_string) {
58 - entry->type = MCP_REQUEST_ID_TYPE_STRING;
59 - entry->str_value = string_strdupz(json_object_get_string(id_obj));
60 - }
61 - else {
62 - // Unsupported ID type, treat as no ID
63 - freez(entry);
64 - return 0;
65 - }
66 -
67 - // Store the entry in the JudyL
68 - *PValue = entry;
69 -
70 - return id;
71 -}
72 -
73 -/**
74 - * Delete a request ID from the registry
75 - *
76 - * @param mcpc The MCP client context
77 - * @param id The request ID to delete
78 - */
79 -void mcp_request_id_del(MCP_CLIENT *mcpc, MCP_REQUEST_ID id) {
80 - if (!mcpc || id == 0)
81 - return;
82 -
83 - // Get the entry from JudyL
84 - Word_t Index = (Word_t)id;
85 - Pvoid_t *PValue = JudyLGet(mcpc->request_ids, Index, NULL);
86 - if (!PValue)
87 - return;
88 -
89 - MCP_REQUEST_ID_ENTRY *entry = *PValue;
90 -
91 - // Free string value if present
92 - if (entry->type == MCP_REQUEST_ID_TYPE_STRING)
93 - string_freez(entry->str_value);
94 -
95 - // Free the entry
96 - freez(entry);
97 -
98 - // Remove the entry from JudyL
99 - int rc = JudyLDel(&mcpc->request_ids, Index, NULL);
100 - if (unlikely(!rc)) {
101 - netdata_log_error("MCP: JudyLDel failed for request ID %zu", id);
102 - }
103 -}
104 -
105 -/**
106 - * Clean up all request IDs for a client
107 - *
108 - * @param mcpc The MCP client context
109 - */
110 -void mcp_request_id_cleanup_all(MCP_CLIENT *mcpc) {
111 - if (!mcpc || !mcpc->request_ids)
112 - return;
113 -
114 - Word_t Index = 0;
115 - Pvoid_t *PValue;
116 -
117 - // Get the first index
118 - PValue = JudyLFirst(mcpc->request_ids, &Index, NULL);
119 -
120 - // Iterate through all entries
121 - while (PValue != NULL) {
122 - // Free the request ID entry
123 - MCP_REQUEST_ID_ENTRY *entry = *PValue;
124 - if (entry->type == MCP_REQUEST_ID_TYPE_STRING)
125 - string_freez(entry->str_value);
126 - freez(entry);
127 -
128 - // Move to next entry
129 - PValue = JudyLNext(mcpc->request_ids, &Index, NULL);
130 - }
131 -
132 - // Free the JudyL array
133 - JudyLFreeArray(&mcpc->request_ids, NULL);
134 - mcpc->request_ids = NULL;
135 -}
136 -
137 -/**
138 - * Add a request ID to a buffer as a JSON member
139 - *
140 - * @param mcpc The MCP client context
141 - * @param wb The buffer to add the ID to
142 - * @param key The JSON key name to use
143 - * @param id The request ID to add
144 - */
145 -void mcp_request_id_to_buffer(MCP_CLIENT *mcpc, BUFFER *wb, const char *key, MCP_REQUEST_ID id) {
146 - if (!wb || !key) {
147 - return;
148 - }
149 -
150 - if (!mcpc || id == 0) {
151 - // For ID 0 or no client context, add it as a numeric 0
152 - buffer_json_member_add_uint64(wb, key, 0);
153 - return;
154 - }
155 -
156 - // Get the entry from JudyL
157 - Word_t Index = (Word_t)id;
158 - Pvoid_t *PValue = JudyLGet(mcpc->request_ids, Index, NULL);
159 - if (!PValue) {
160 - // If entry not found, add 0 as the ID
161 - buffer_json_member_add_uint64(wb, key, 0);
162 - return;
163 - }
164 -
165 - MCP_REQUEST_ID_ENTRY *entry = *PValue;
166 -
167 - // Add the ID based on its type
168 - if (entry->type == MCP_REQUEST_ID_TYPE_INT) {
169 - buffer_json_member_add_uint64(wb, key, entry->int_value);
170 - }
171 - else if (entry->type == MCP_REQUEST_ID_TYPE_STRING) {
172 - buffer_json_member_add_string(wb, key, string2str(entry->str_value));
173 - }
174 -}
\ No newline at end of file
src/web/mcp/mcp-request-id.h deleted
-48
@@ -1,48 +0,0 @@
1 -// SPDX-License-Identifier: GPL-3.0-or-later
2 -
3 -#ifndef NETDATA_MCP_REQUEST_ID_H
4 -#define NETDATA_MCP_REQUEST_ID_H
5 -
6 -#include "libnetdata/libnetdata.h"
7 -
8 -// Request ID type - 0 is reserved for "no ID given"
9 -typedef size_t MCP_REQUEST_ID;
10 -
11 -// Forward declaration
12 -struct mcp_client;
13 -
14 -/**
15 - * Extract and register a request ID from a JSON object
16 - *
17 - * @param mcpc The MCP client context
18 - * @param request The JSON request object that may contain an ID
19 - * @return MCP_REQUEST_ID - the assigned ID (0 if no ID was present)
20 - */
21 -MCP_REQUEST_ID mcp_request_id_add(struct mcp_client *mcpc, struct json_object *request);
22 -
23 -/**
24 - * Delete a request ID from the registry
25 - *
26 - * @param mcpc The MCP client context
27 - * @param id The request ID to delete
28 - */
29 -void mcp_request_id_del(struct mcp_client *mcpc, MCP_REQUEST_ID id);
30 -
31 -/**
32 - * Clean up all request IDs for a client
33 - *
34 - * @param mcpc The MCP client context
35 - */
36 -void mcp_request_id_cleanup_all(struct mcp_client *mcpc);
37 -
38 -/**
39 - * Add a request ID to a buffer as a JSON member
40 - *
41 - * @param mcpc The MCP client context
42 - * @param wb The buffer to add the ID to
43 - * @param key The JSON key name to use
44 - * @param id The request ID to add
45 - */
46 -void mcp_request_id_to_buffer(struct mcp_client *mcpc, BUFFER *wb, const char *key, MCP_REQUEST_ID id);
47 -
48 -#endif // NETDATA_MCP_REQUEST_ID_H
src/web/mcp/mcp-resources.c
+11 -15
@@ -83,8 +83,8 @@ typedef struct {
83 } MCP_RESOURCE_TEMPLATE;
84
85 // Implementation of resources/list
86 -static MCP_RETURN_CODE mcp_resources_method_list(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) {
87 - if (!mcpc || !params || !id) return MCP_RC_INTERNAL_ERROR;
86 +static MCP_RETURN_CODE mcp_resources_method_list(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id __maybe_unused) {
87 + if (!mcpc || !params) return MCP_RC_INTERNAL_ERROR;
88
89 // Initialize success response
90 mcp_init_success_result(mcpc, id);
@@ -98,8 +98,8 @@ static MCP_RETURN_CODE mcp_resources_method_list(MCP_CLIENT *mcpc, struct json_o
98 }
99
100 // Implementation of resources/read
101 -static MCP_RETURN_CODE mcp_resources_method_read(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) {
102 - if (!mcpc || id == 0 || !params) return MCP_RC_INTERNAL_ERROR;
101 +static MCP_RETURN_CODE mcp_resources_method_read(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id __maybe_unused) {
102 + if (!mcpc || !params) return MCP_RC_INTERNAL_ERROR;
103
104 // Extract URI from params
105 struct json_object *uri_obj = NULL;
@@ -122,8 +122,8 @@ static MCP_RETURN_CODE mcp_resources_method_read(MCP_CLIENT *mcpc, struct json_o
122 }
123
124 // Implementation of resources/templates/list
125 -static MCP_RETURN_CODE mcp_resources_method_templates_list(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) {
126 - if (!mcpc || !params || !id) return MCP_RC_INTERNAL_ERROR;
125 +static MCP_RETURN_CODE mcp_resources_method_templates_list(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id __maybe_unused) {
126 + if (!mcpc || !params) return MCP_RC_INTERNAL_ERROR;
127
128 // Initialize success response
129 mcp_init_success_result(mcpc, id);
@@ -137,27 +137,23 @@ static MCP_RETURN_CODE mcp_resources_method_templates_list(MCP_CLIENT *mcpc, str
137 }
138
139 // Implementation of resources/subscribe (transport-agnostic)
140 -static MCP_RETURN_CODE mcp_resources_method_subscribe(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) {
141 - if (!mcpc || !id || !params) return MCP_RC_INTERNAL_ERROR;
140 +static MCP_RETURN_CODE mcp_resources_method_subscribe(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id __maybe_unused) {
141 + if (!mcpc || !params) return MCP_RC_INTERNAL_ERROR;
142 return MCP_RC_NOT_IMPLEMENTED;
143 }
144
145 // Implementation of resources/unsubscribe (transport-agnostic)
146 -static MCP_RETURN_CODE mcp_resources_method_unsubscribe(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) {
147 - if (!mcpc || id == 0 || !params) return MCP_RC_INTERNAL_ERROR;
146 +static MCP_RETURN_CODE mcp_resources_method_unsubscribe(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id __maybe_unused) {
147 + if (!mcpc || !params) return MCP_RC_INTERNAL_ERROR;
148 return MCP_RC_NOT_IMPLEMENTED;
149 }
150
151 // Resource namespace method dispatcher (transport-agnostic)
152 -MCP_RETURN_CODE mcp_resources_route(MCP_CLIENT *mcpc, const char *method, struct json_object *params, MCP_REQUEST_ID id) {
152 +MCP_RETURN_CODE mcp_resources_route(MCP_CLIENT *mcpc, const char *method, struct json_object *params, MCP_REQUEST_ID id __maybe_unused) {
153 if (!mcpc || !method) return MCP_RC_INTERNAL_ERROR;
154
155 netdata_log_debug(D_MCP, "MCP resources method: %s", method);
156
157 - // Clear previous buffers
158 - buffer_flush(mcpc->result);
159 - buffer_flush(mcpc->error);
160 -
157 MCP_RETURN_CODE rc;
158
159 if (strcmp(method, "list") == 0) {
src/web/mcp/mcp-test-client/README.md
+8 -7
@@ -1,10 +1,10 @@
1 # Netdata MCP Web Client
2
3 -A web-based client for testing and interacting with Netdata's Model Context Protocol (MCP) server via WebSocket.
3 +A web-based client for testing and interacting with Netdata's Model Context Protocol (MCP) server over WebSocket, streamable HTTP, or Server-Sent Events (SSE).
4
5 ## Features
6
7 -- **WebSocket Connection**: Connect to any MCP server via WebSocket
7 +- **Multi-transport support**: Connect to MCP over WebSocket, HTTP chunked responses, or SSE
8 - **Schema Validation**: Validates tool schemas against MCP specification
9 - **Custom UI Generator**: Lightweight form generator for tool parameters
10 - **JSON Pretty Printing**: Advanced formatting with syntax highlighting
@@ -20,11 +20,12 @@ A web-based client for testing and interacting with Netdata's Model Context Prot
20 ## Usage
21
22 1. Open `index.html` in a web browser
23 -2. Enter your MCP WebSocket URL (default: `ws://localhost:19999/mcp`)
24 -3. Click "Connect"
23 +2. Enter your MCP endpoint URL (defaults to `ws://localhost:19999/mcp`)
24 + - WebSocket URLs (`ws://` / `wss://`) connect automatically over WebSocket
25 + - HTTP/HTTPS URLs show a selector to choose between **Streamable HTTP** and **SSE**
26 +3. Click "Connect" or "Connect and Handshake" to run the full capability discovery flow
27 4. Use the interface to:
26 - - Initialize the connection
27 - - List available tools
28 + - Initialize the connection and fetch tool, prompt, and resource lists automatically
29 - Call tools with parameters
30 - View formatted responses
31
@@ -60,4 +61,4 @@ To extend or modify the client:
61
62 - Modern browser with WebSocket support
63 - JavaScript enabled
63 -- No external dependencies required
\ No newline at end of file
64 +- No external dependencies required
src/web/mcp/mcp-test-client/index.html
+1564 -392
@@ -37,6 +37,160 @@
37 flex-wrap: wrap;
38 gap: 8px;
39 }
40 + .server-selector {
41 + position: relative;
42 + display: inline-flex;
43 + align-items: center;
44 + }
45 + .server-dropdown-btn {
46 + display: inline-flex;
47 + align-items: center;
48 + gap: 6px;
49 + padding: 6px 10px;
50 + border: 1px solid #99c6dd;
51 + border-radius: 4px;
52 + background-color: #fff;
53 + color: #005f8a;
54 + font-size: 0.95em;
55 + cursor: pointer;
56 + min-width: 220px;
57 + }
58 + .server-dropdown-btn:hover {
59 + background-color: #f0f8ff;
60 + }
61 + .server-dropdown-btn .caret {
62 + margin-left: auto;
63 + font-size: 0.9em;
64 + }
65 + .server-dropdown-menu {
66 + position: absolute;
67 + top: calc(100% + 6px);
68 + left: 0;
69 + min-width: 280px;
70 + background-color: white;
71 + border: 1px solid #99c6dd;
72 + border-radius: 6px;
73 + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.12);
74 + z-index: 200;
75 + display: none;
76 + max-height: 320px;
77 + overflow-y: auto;
78 + }
79 + .server-dropdown-menu.open {
80 + display: block;
81 + }
82 + .server-menu-header {
83 + display: flex;
84 + align-items: center;
85 + justify-content: space-between;
86 + padding: 8px 10px;
87 + border-bottom: 1px solid #ddeaf3;
88 + background-color: #f5fbff;
89 + font-size: 0.9em;
90 + font-weight: bold;
91 + color: #005f8a;
92 + }
93 + .server-menu-add {
94 + background-color: #28a745;
95 + color: white;
96 + border: none;
97 + border-radius: 4px;
98 + padding: 4px 8px;
99 + font-size: 0.85em;
100 + cursor: pointer;
101 + }
102 + .server-menu-add:hover {
103 + background-color: #218838;
104 + }
105 + .server-menu-list {
106 + display: flex;
107 + flex-direction: column;
108 + }
109 + .server-menu-empty {
110 + padding: 12px 14px;
111 + font-size: 0.85em;
112 + color: #666;
113 + text-align: center;
114 + }
115 + .server-menu-item {
116 + display: flex;
117 + align-items: center;
118 + justify-content: space-between;
119 + gap: 10px;
120 + padding: 8px 10px;
121 + cursor: pointer;
122 + border-bottom: 1px solid #f0f4f7;
123 + }
124 + .server-menu-item:last-child {
125 + border-bottom: none;
126 + }
127 + .server-menu-item:hover {
128 + background-color: #f0f8ff;
129 + }
130 + .server-menu-item.active {
131 + background-color: #d0e8f2;
132 + font-weight: bold;
133 + }
134 + .server-menu-info {
135 + display: flex;
136 + flex-direction: column;
137 + gap: 2px;
138 + flex: 1;
139 + }
140 + .server-menu-url {
141 + font-size: 0.9em;
142 + color: #003f5f;
143 + word-break: break-all;
144 + }
145 + .server-menu-meta {
146 + display: flex;
147 + align-items: center;
148 + gap: 8px;
149 + font-size: 0.8em;
150 + color: #666;
151 + }
152 + .server-menu-bearer {
153 + color: #0f7b0f;
154 + font-weight: bold;
155 + }
156 + .server-menu-actions {
157 + display: flex;
158 + gap: 6px;
159 + }
160 + .server-menu-btn {
161 + border: 1px solid #99c6dd;
162 + background-color: #f5fbff;
163 + color: #005f8a;
164 + border-radius: 4px;
165 + font-size: 0.78em;
166 + padding: 4px 6px;
167 + cursor: pointer;
168 + }
169 + .server-menu-btn:hover {
170 + background-color: #e0f0ff;
171 + }
172 + .server-menu-btn.delete {
173 + border-color: #d9534f;
174 + color: #d9534f;
175 + background-color: #fff5f5;
176 + }
177 + .server-menu-btn.delete:hover {
178 + background-color: #ffe5e5;
179 + }
180 + .transport-select {
181 + display: none;
182 + align-items: center;
183 + gap: 6px;
184 + }
185 + .transport-select label {
186 + font-size: 0.9em;
187 + color: #005f8a;
188 + }
189 + .transport-select select {
190 + padding: 4px 6px;
191 + border-radius: 4px;
192 + border: 1px solid #99c6dd;
193 + }
194 .four-column-layout {
195 display: grid;
196 grid-template-columns: 112px 216px 1fr 2fr;
@@ -593,12 +747,75 @@
747 margin-bottom: 10px;
748 color: #666;
749 }
750 + #serverModal .modal-content {
751 + max-width: 420px;
752 + }
753 + .server-modal-form {
754 + display: flex;
755 + flex-direction: column;
756 + gap: 12px;
757 + }
758 + .server-modal-form label {
759 + font-size: 0.85em;
760 + color: #005f8a;
761 + margin-bottom: 4px;
762 + }
763 + .server-modal-form input,
764 + .server-modal-form select {
765 + padding: 6px 8px;
766 + border: 1px solid #99c6dd;
767 + border-radius: 4px;
768 + font-size: 0.9em;
769 + }
770 + .server-bearer-wrapper {
771 + display: flex;
772 + gap: 6px;
773 + align-items: center;
774 + }
775 + .toggle-visibility-btn {
776 + border: 1px solid #99c6dd;
777 + background-color: #f5fbff;
778 + color: #005f8a;
779 + border-radius: 4px;
780 + font-size: 0.85em;
781 + padding: 4px 8px;
782 + cursor: pointer;
783 + white-space: nowrap;
784 + }
785 + .toggle-visibility-btn:hover {
786 + background-color: #e0f0ff;
787 + }
788 + .form-hint {
789 + font-size: 0.75em;
790 + color: #666;
791 + }
792 </style>
793 </head>
794 <body>
795 <div class="connection-panel">
600 - <label for="serverUrl">Netdata MCP Test Client:</label>
601 - <input type="text" id="serverUrl" value="ws://localhost:19999/mcp" style="width: 300px;">
796 + <label for="serverDropdownButton">Netdata MCP Test Client:</label>
797 + <div class="server-selector">
798 + <button type="button" id="serverDropdownButton" class="server-dropdown-btn">
799 + <span id="serverDropdownLabel">Servers</span>
800 + <span class="caret">v</span>
801 + </button>
802 + <div id="serverDropdownMenu" class="server-dropdown-menu">
803 + <div class="server-menu-header">
804 + <span>Saved Servers</span>
805 + <button type="button" id="addServerBtn" class="server-menu-add">Add</button>
806 + </div>
807 + <div id="serverListContainer" class="server-menu-list"></div>
808 + <div id="serverEmptyState" class="server-menu-empty" style="display: none;">No servers saved yet.</div>
809 + </div>
810 + </div>
811 + <input type="hidden" id="serverUrl" value="ws://localhost:19999/mcp">
812 + <span id="httpTransportSelectWrapper" class="transport-select">
813 + <label for="httpTransportSelect">Transport:</label>
814 + <select id="httpTransportSelect">
815 + <option value="stream-http">Streamable HTTP</option>
816 + <option value="sse">Server-Sent Events</option>
817 + </select>
818 + </span>
819 <button id="connectBtn">Connect</button>
820 <button id="connectAndInitBtn">Connect and Handshake</button>
821 <button id="disconnectBtn" disabled>Disconnect</button>
@@ -694,6 +911,44 @@
911 </div>
912 </div>
913
914 + <!-- Manage Servers Modal -->
915 + <div id="serverModal" class="modal">
916 + <div class="modal-content">
917 + <div class="modal-header">
918 + <h2 id="serverModalTitle">Add Server</h2>
919 + <span class="modal-close" id="serverModalClose">&times;</span>
920 + </div>
921 + <div class="modal-body">
922 + <form class="server-modal-form" onsubmit="return false;">
923 + <div>
924 + <label for="serverModalUrl">Server URL</label>
925 + <input type="text" id="serverModalUrl" placeholder="https://example.com/mcp" autocomplete="off">
926 + </div>
927 + <div>
928 + <label for="serverModalType">Transport Type</label>
929 + <select id="serverModalType">
930 + <option value="websocket">WebSocket</option>
931 + <option value="stream-http">Streamable HTTP</option>
932 + <option value="sse">Server-Sent Events</option>
933 + </select>
934 + </div>
935 + <div>
936 + <label for="serverModalToken">Bearer Token</label>
937 + <div class="server-bearer-wrapper">
938 + <input type="password" id="serverModalToken" autocomplete="new-password" placeholder="Optional" spellcheck="false">
939 + <button type="button" id="toggleServerTokenVisibility" class="toggle-visibility-btn">Show</button>
940 + </div>
941 + <div class="form-hint">Leave blank if the server does not require authorization.</div>
942 + </div>
943 + </form>
944 + </div>
945 + <div class="modal-footer">
946 + <button type="button" id="serverModalCancel">Cancel</button>
947 + <button type="button" id="serverModalSave" style="background-color: #28a745;">Save</button>
948 + </div>
949 + </div>
950 + </div>
951 +
952 <!-- Import from LLM Modal -->
953 <div id="importLLMModal" class="modal">
954 <div class="modal-content">
@@ -739,15 +994,29 @@
994 let schemaFormGenerator = null;
995 let currentMethodSchema = null;
996 let jsonPrinter = null;
742 - let pendingRequests = new Map(); // Track request timestamps by ID
997 + let pendingRequests = new Map(); // Track request metadata by ID
998 let responseEntries = []; // Track response entries for navigation
999 let currentResponseIndex = -1;
745 -
1000 + let currentTransport = 'websocket';
1001 + let httpTransportPreference = 'stream-http';
1002 + let currentServerUrl = '';
1003 + let isConnected = false;
1004 + let activeSseController = null;
1005 + let serverEntries = [];
1006 + let selectedServerId = null;
1007 + let serverModalMode = 'add';
1008 + let serverModalEditingId = null;
1009 +
1010 + const DEFAULT_WS_URL = 'ws://localhost:19999/mcp';
1011 +
1012 // Local storage keys
1013 const STORAGE_KEYS = {
1014 TOOL_PARAMS: 'mcp_tool_params',
1015 REQUEST_HISTORY: 'mcp_request_history',
750 - SERVER_URL: 'mcp_server_url'
1016 + SERVER_URL: 'mcp_server_url',
1017 + HTTP_TRANSPORT: 'mcp_http_transport',
1018 + SERVER_ENTRIES: 'mcp_server_entries_v1',
1019 + SELECTED_SERVER_ID: 'mcp_selected_server_v1'
1020 };
1021
1022 // Local Storage utility functions
@@ -768,6 +1037,61 @@
1037 return defaultValue;
1038 }
1039 }
1040 +
1041 + function removeFromLocalStorage(key) {
1042 + try {
1043 + localStorage.removeItem(key);
1044 + } catch (e) {
1045 + console.warn('Failed to remove from localStorage:', e);
1046 + }
1047 + }
1048 +
1049 + function generateServerId() {
1050 + return 'srv_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
1051 + }
1052 +
1053 + function loadServerEntries() {
1054 + const entries = loadFromLocalStorage(STORAGE_KEYS.SERVER_ENTRIES, []);
1055 + return Array.isArray(entries) ? entries : [];
1056 + }
1057 +
1058 + function saveServerEntries(entries) {
1059 + serverEntries = Array.isArray(entries) ? entries : [];
1060 + saveToLocalStorage(STORAGE_KEYS.SERVER_ENTRIES, serverEntries);
1061 + }
1062 +
1063 + function loadSelectedServerKey() {
1064 + return loadFromLocalStorage(STORAGE_KEYS.SELECTED_SERVER_ID, null);
1065 + }
1066 +
1067 + function saveSelectedServerKey(id) {
1068 + if (id) {
1069 + saveToLocalStorage(STORAGE_KEYS.SELECTED_SERVER_ID, id);
1070 + } else {
1071 + removeFromLocalStorage(STORAGE_KEYS.SELECTED_SERVER_ID);
1072 + }
1073 + }
1074 +
1075 + function getServerById(id) {
1076 + if (!id) {
1077 + return null;
1078 + }
1079 + return serverEntries.find(server => server.id === id) || null;
1080 + }
1081 +
1082 + function getSelectedServer() {
1083 + return getServerById(selectedServerId);
1084 + }
1085 +
1086 + const TRANSPORT_LABELS = {
1087 + websocket: 'WebSocket',
1088 + 'stream-http': 'Streamable HTTP',
1089 + sse: 'Server-Sent Events'
1090 + };
1091 +
1092 + function formatTransportLabel(type) {
1093 + return TRANSPORT_LABELS[type] || type;
1094 + }
1095
1096 // Save tool parameters
1097 function saveToolParams(toolName, params) {
@@ -818,11 +1142,19 @@
1142 function saveServerUrl(url) {
1143 saveToLocalStorage(STORAGE_KEYS.SERVER_URL, url);
1144 }
821 -
1145 +
1146 // Load server URL
1147 function loadServerUrl() {
1148 return loadFromLocalStorage(STORAGE_KEYS.SERVER_URL, 'ws://localhost:19999/mcp');
1149 }
1150 +
1151 + function saveHttpTransportPreference(transport) {
1152 + saveToLocalStorage(STORAGE_KEYS.HTTP_TRANSPORT, transport);
1153 + }
1154 +
1155 + function loadHttpTransportPreference() {
1156 + return loadFromLocalStorage(STORAGE_KEYS.HTTP_TRANSPORT, 'stream-http');
1157 + }
1158
1159 // Clear saved parameters for current tool
1160 function clearCurrentToolParams() {
@@ -1118,15 +1450,32 @@
1450 const jsonEditor = document.getElementById('jsonEditor');
1451 const responseViewer = document.getElementById('responseViewer');
1452 const statusElement = document.getElementById('status');
1453 + const serverDropdownButton = document.getElementById('serverDropdownButton');
1454 + const serverDropdownLabel = document.getElementById('serverDropdownLabel');
1455 + const serverDropdownMenu = document.getElementById('serverDropdownMenu');
1456 + const serverListContainer = document.getElementById('serverListContainer');
1457 + const serverEmptyState = document.getElementById('serverEmptyState');
1458 + const addServerBtn = document.getElementById('addServerBtn');
1459 const serverUrlInput = document.getElementById('serverUrl');
1460 + const httpTransportSelectWrapper = document.getElementById('httpTransportSelectWrapper');
1461 + const httpTransportSelect = document.getElementById('httpTransportSelect');
1462 const flowsList = document.getElementById('flowsList');
1463 const methodsList = document.getElementById('methodsList');
1464 const noSchemaMessage = document.getElementById('noSchemaMessage');
1465 const schemaFormEditor = document.getElementById('schemaFormEditor');
1466 + const serverModal = document.getElementById('serverModal');
1467 + const serverModalTitle = document.getElementById('serverModalTitle');
1468 + const serverModalUrl = document.getElementById('serverModalUrl');
1469 + const serverModalType = document.getElementById('serverModalType');
1470 + const serverModalToken = document.getElementById('serverModalToken');
1471 + const toggleServerTokenVisibility = document.getElementById('toggleServerTokenVisibility');
1472 + const serverModalCancel = document.getElementById('serverModalCancel');
1473 + const serverModalSave = document.getElementById('serverModalSave');
1474 + const serverModalClose = document.getElementById('serverModalClose');
1475
1476 // Event listeners
1128 - connectBtn.addEventListener('click', connect);
1129 - connectAndInitBtn.addEventListener('click', connectAndInitialize);
1477 + connectBtn.addEventListener('click', () => { connect().catch(err => log('Connection error: ' + err.message)); });
1478 + connectAndInitBtn.addEventListener('click', () => { connectAndInitialize().catch(err => log('Handshake error: ' + err.message)); });
1479 disconnectBtn.addEventListener('click', disconnect);
1480 sendBtn.addEventListener('click', sendRequest);
1481 sendFromFormBtn.addEventListener('click', sendFromForm);
@@ -1140,88 +1489,529 @@
1489 nextResponseBtn.addEventListener('click', navigateToNextResponse);
1490 clearParamsBtn.addEventListener('click', clearCurrentToolParams);
1491 clearHistoryBtn.addEventListener('click', clearRequestHistory);
1143 -
1144 - // JSON editor change detection
1145 - let jsonEditorChangeTimer = null;
1146 -
1147 - jsonEditor.addEventListener('input', () => {
1148 - // Skip if this change was triggered by code (not user)
1149 - if (isUpdatingFromCode) return;
1150 -
1151 - // Debounce the update to avoid excessive updates while typing
1152 - clearTimeout(jsonEditorChangeTimer);
1153 - jsonEditorChangeTimer = setTimeout(() => {
1154 - // Only update form if we're on the edit tab and have a schema
1155 - if (currentActiveTab === 'editRequest' && schemaFormGenerator) {
1156 - updateFormFromRaw();
1492 + httpTransportSelect.addEventListener('change', () => {
1493 + httpTransportPreference = httpTransportSelect.value;
1494 + saveHttpTransportPreference(httpTransportPreference);
1495 + });
1496 + serverDropdownButton.addEventListener('click', toggleServerDropdown);
1497 + addServerBtn.addEventListener('click', () => {
1498 + closeServerDropdown();
1499 + openServerModal();
1500 + });
1501 + serverListContainer.addEventListener('click', handleServerListInteraction);
1502 + document.addEventListener('click', (event) => {
1503 + if (!serverDropdownMenu.classList.contains('open')) {
1504 + return;
1505 + }
1506 + if (event.target.closest('.server-selector')) {
1507 + return;
1508 + }
1509 + closeServerDropdown();
1510 + });
1511 + document.addEventListener('keydown', (event) => {
1512 + if (event.key === 'Escape') {
1513 + if (serverDropdownMenu.classList.contains('open')) {
1514 + closeServerDropdown();
1515 }
1158 - }, 500); // 500ms delay
1516 + if (serverModal.style.display === 'block') {
1517 + closeServerModal();
1518 + }
1519 + }
1520 });
1160 -
1161 - // Flow selection
1162 - flowsList.addEventListener('click', (e) => {
1163 - const flowItem = e.target.closest('.flow-item');
1164 - if (flowItem) {
1165 - document.querySelectorAll('.flow-item').forEach(item => item.classList.remove('active'));
1166 - flowItem.classList.add('active');
1167 - const flowName = flowItem.dataset.flow;
1168 - displayMethods(flowName);
1521 + serverModalCancel.addEventListener('click', closeServerModal);
1522 + serverModalClose.addEventListener('click', closeServerModal);
1523 + serverModalSave.addEventListener('click', handleServerModalSave);
1524 + toggleServerTokenVisibility.addEventListener('click', toggleBearerVisibility);
1525 + serverModal.addEventListener('click', (event) => {
1526 + if (event.target === serverModal) {
1527 + closeServerModal();
1528 }
1529 });
1171 -
1172 - // Initialize from storage
1173 - function initializeFromStorage() {
1174 - // Load server URL
1175 - const savedServerUrl = loadServerUrl();
1176 - if (savedServerUrl) {
1177 - serverUrlInput.value = savedServerUrl;
1530 +
1531 + // JSON editor change detection
1532 + let jsonEditorChangeTimer = null;
1533 +
1534 + function toggleServerDropdown() {
1535 + if (serverDropdownMenu.classList.contains('open')) {
1536 + closeServerDropdown();
1537 + return;
1538 }
1179 -
1180 - // Load request history
1181 - requestHistory = loadRequestHistory();
1182 -
1183 - // Update history flow indicator
1184 - updateHistoryFlowIndicator();
1539 + renderServerDropdown();
1540 + const buttonWidth = serverDropdownButton.getBoundingClientRect().width;
1541 + serverDropdownMenu.style.minWidth = Math.max(280, Math.ceil(buttonWidth)) + 'px';
1542 + serverDropdownMenu.classList.add('open');
1543 }
1186 -
1187 - // Update history flow indicator
1188 - function updateHistoryFlowIndicator() {
1189 - const historyCount = loadRequestHistory().length;
1190 - const historyFlow = document.querySelector('[data-flow="custom"]');
1191 -
1192 - if (historyCount > 0) {
1193 - historyFlow.textContent = `History (${historyCount})`;
1194 - historyFlow.style.fontWeight = 'bold';
1195 - historyFlow.style.color = '#0088cc';
1196 - } else {
1197 - historyFlow.textContent = 'History';
1198 - historyFlow.style.fontWeight = '';
1199 - historyFlow.style.color = '';
1544 +
1545 + function closeServerDropdown() {
1546 + serverDropdownMenu.classList.remove('open');
1547 + }
1548 +
1549 + function handleServerListInteraction(event) {
1550 + const action = event.target.dataset.action;
1551 + const serverId = event.target.dataset.serverId;
1552 +
1553 + if (action === 'delete') {
1554 + event.stopPropagation();
1555 + deleteServer(serverId);
1556 + return;
1557 + }
1558 +
1559 + if (action === 'edit') {
1560 + event.stopPropagation();
1561 + closeServerDropdown();
1562 + openServerModal({ mode: 'edit', serverId });
1563 + return;
1564 + }
1565 +
1566 + const item = event.target.closest('.server-menu-item');
1567 + if (item) {
1568 + selectServer(item.dataset.serverId);
1569 + closeServerDropdown();
1570 }
1571 }
1202 -
1203 - // Initialize JSON pretty printer
1204 - jsonPrinter = new JSONPrettyPrinter({
1205 - indent: 2,
1206 - visualizeNewlines: true,
1207 - detectNestedJSON: true,
1208 - syntaxHighlight: true
1209 - });
1210 -
1211 - // Initialize with saved data
1212 - initializeFromStorage();
1213 -
1214 - // Initialize with first flow
1215 - displayMethods('initialization');
1216 - updateNavButtons();
1217 -
1218 - // Keyboard shortcuts for response navigation
1219 - document.addEventListener('keydown', (e) => {
1220 - // Only work when not focused on an input field
1221 - if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
1572 +
1573 + function renderServerDropdown() {
1574 + if (!serverListContainer) {
1575 return;
1576 }
1224 -
1577 +
1578 + serverListContainer.innerHTML = '';
1579 +
1580 + if (!serverEntries.length) {
1581 + serverEmptyState.style.display = 'block';
1582 + return;
1583 + }
1584 +
1585 + serverEmptyState.style.display = 'none';
1586 +
1587 + serverEntries.forEach((server) => {
1588 + const item = document.createElement('div');
1589 + item.className = 'server-menu-item';
1590 + item.dataset.serverId = server.id;
1591 + if (server.id === selectedServerId) {
1592 + item.classList.add('active');
1593 + }
1594 +
1595 + const info = document.createElement('div');
1596 + info.className = 'server-menu-info';
1597 +
1598 + const urlElem = document.createElement('div');
1599 + urlElem.className = 'server-menu-url';
1600 + urlElem.textContent = server.url;
1601 + info.appendChild(urlElem);
1602 +
1603 + const meta = document.createElement('div');
1604 + meta.className = 'server-menu-meta';
1605 +
1606 + const typeElem = document.createElement('span');
1607 + typeElem.className = 'server-menu-type';
1608 + typeElem.textContent = formatTransportLabel(server.type);
1609 + meta.appendChild(typeElem);
1610 +
1611 + if (server.bearerToken) {
1612 + const bearerElem = document.createElement('span');
1613 + bearerElem.className = 'server-menu-bearer';
1614 + bearerElem.title = 'Bearer token configured';
1615 + bearerElem.textContent = '✓';
1616 + meta.appendChild(bearerElem);
1617 + }
1618 +
1619 + info.appendChild(meta);
1620 + item.appendChild(info);
1621 +
1622 + const actions = document.createElement('div');
1623 + actions.className = 'server-menu-actions';
1624 +
1625 + const editBtn = document.createElement('button');
1626 + editBtn.type = 'button';
1627 + editBtn.className = 'server-menu-btn edit';
1628 + editBtn.dataset.action = 'edit';
1629 + editBtn.dataset.serverId = server.id;
1630 + editBtn.textContent = 'Edit';
1631 + editBtn.title = 'Edit server';
1632 + actions.appendChild(editBtn);
1633 +
1634 + const deleteBtn = document.createElement('button');
1635 + deleteBtn.type = 'button';
1636 + deleteBtn.className = 'server-menu-btn delete';
1637 + deleteBtn.dataset.action = 'delete';
1638 + deleteBtn.dataset.serverId = server.id;
1639 + deleteBtn.textContent = 'Delete';
1640 + deleteBtn.title = 'Delete server';
1641 + actions.appendChild(deleteBtn);
1642 +
1643 + item.appendChild(actions);
1644 + serverListContainer.appendChild(item);
1645 + });
1646 + }
1647 +
1648 + function persistSelectedServer(server) {
1649 + if (!server) {
1650 + selectedServerId = null;
1651 + saveSelectedServerKey(null);
1652 + return;
1653 + }
1654 +
1655 + selectedServerId = server.id;
1656 + saveSelectedServerKey(server.id);
1657 + saveServerUrl(server.url);
1658 +
1659 + if (server.type === 'stream-http' || server.type === 'sse') {
1660 + httpTransportPreference = server.type;
1661 + saveHttpTransportPreference(server.type);
1662 + }
1663 + }
1664 +
1665 + function updateTransportControlsForServer(server) {
1666 + if (server && (server.type === 'stream-http' || server.type === 'sse')) {
1667 + httpTransportPreference = server.type;
1668 + httpTransportSelect.value = server.type;
1669 + httpTransportSelect.disabled = true;
1670 + httpTransportSelectWrapper.style.display = 'inline-flex';
1671 + } else {
1672 + httpTransportSelectWrapper.style.display = 'none';
1673 + httpTransportSelect.disabled = false;
1674 + }
1675 + }
1676 +
1677 + function applyServerSelection(server) {
1678 + if (!server) {
1679 + serverDropdownLabel.textContent = 'Servers';
1680 + serverUrlInput.value = '';
1681 + currentServerUrl = '';
1682 + updateTransportControlsForServer(null);
1683 + return;
1684 + }
1685 +
1686 + serverDropdownLabel.textContent = server.url;
1687 + serverUrlInput.value = server.url;
1688 + currentServerUrl = server.url;
1689 + updateTransportControlsForServer(server);
1690 + }
1691 +
1692 + function selectServer(serverId) {
1693 + const server = getServerById(serverId);
1694 + if (!server) {
1695 + return;
1696 + }
1697 +
1698 + persistSelectedServer(server);
1699 + applyServerSelection(server);
1700 + renderServerDropdown();
1701 + }
1702 +
1703 + function deleteServer(serverId) {
1704 + const server = getServerById(serverId);
1705 + if (!server) {
1706 + return;
1707 + }
1708 +
1709 + if (!confirm(`Delete server "${server.url}"?`)) {
1710 + return;
1711 + }
1712 +
1713 + serverEntries = serverEntries.filter(entry => entry.id !== serverId);
1714 + saveServerEntries(serverEntries);
1715 +
1716 + if (selectedServerId === serverId) {
1717 + if (serverEntries.length) {
1718 + const fallback = serverEntries[0];
1719 + persistSelectedServer(fallback);
1720 + applyServerSelection(fallback);
1721 + } else {
1722 + persistSelectedServer(null);
1723 + applyServerSelection(null);
1724 + }
1725 + }
1726 +
1727 + renderServerDropdown();
1728 + }
1729 +
1730 + function resetServerModal() {
1731 + serverModalUrl.value = '';
1732 + serverModalType.value = 'stream-http';
1733 + serverModalToken.value = '';
1734 + serverModalToken.type = 'password';
1735 + toggleServerTokenVisibility.textContent = 'Show';
1736 + }
1737 +
1738 + function openServerModal({ mode = 'add', serverId = null } = {}) {
1739 + serverModalMode = mode;
1740 + serverModalEditingId = serverId;
1741 +
1742 + if (mode === 'edit') {
1743 + const server = getServerById(serverId);
1744 + if (!server) {
1745 + return;
1746 + }
1747 + serverModalTitle.textContent = 'Edit Server';
1748 + serverModalUrl.value = server.url;
1749 + serverModalType.value = server.type;
1750 + serverModalToken.value = server.bearerToken || '';
1751 + serverModalToken.type = 'password';
1752 + toggleServerTokenVisibility.textContent = 'Show';
1753 + } else {
1754 + serverModalTitle.textContent = 'Add Server';
1755 + resetServerModal();
1756 + const activeServer = getSelectedServer();
1757 + if (activeServer) {
1758 + serverModalType.value = activeServer.type;
1759 + }
1760 + }
1761 +
1762 + serverModal.style.display = 'block';
1763 + setTimeout(() => {
1764 + serverModalUrl.focus();
1765 + }, 0);
1766 + }
1767 +
1768 + function closeServerModal() {
1769 + serverModal.style.display = 'none';
1770 + resetServerModal();
1771 + }
1772 +
1773 + function toggleBearerVisibility() {
1774 + if (serverModalToken.type === 'password') {
1775 + serverModalToken.type = 'text';
1776 + toggleServerTokenVisibility.textContent = 'Hide';
1777 + } else {
1778 + serverModalToken.type = 'password';
1779 + toggleServerTokenVisibility.textContent = 'Show';
1780 + }
1781 + }
1782 +
1783 + function handleServerModalSave() {
1784 + const url = serverModalUrl.value.trim();
1785 + const type = serverModalType.value;
1786 + const bearerToken = serverModalToken.value.trim();
1787 +
1788 + if (!url) {
1789 + alert('Please enter a server URL.');
1790 + return;
1791 + }
1792 +
1793 + if (type === 'websocket' && !isWebSocketUrl(url)) {
1794 + alert('WebSocket URLs must start with ws:// or wss://');
1795 + return;
1796 + }
1797 +
1798 + if ((type === 'stream-http' || type === 'sse') && !isHttpUrl(url)) {
1799 + alert('HTTP transports require URLs that start with http:// or https://');
1800 + return;
1801 + }
1802 +
1803 + if (!['websocket', 'stream-http', 'sse'].includes(type)) {
1804 + alert('Unknown transport type.');
1805 + return;
1806 + }
1807 +
1808 + const sanitizedToken = bearerToken || '';
1809 +
1810 + if (serverModalMode === 'edit') {
1811 + const existing = getServerById(serverModalEditingId);
1812 + if (!existing) {
1813 + alert('Unable to locate server to edit.');
1814 + return;
1815 + }
1816 +
1817 + const updated = {
1818 + id: existing.id,
1819 + url,
1820 + type,
1821 + bearerToken: sanitizedToken
1822 + };
1823 +
1824 + serverEntries = [updated, ...serverEntries.filter(entry => entry.id !== existing.id)];
1825 + saveServerEntries(serverEntries);
1826 + persistSelectedServer(updated);
1827 + applyServerSelection(updated);
1828 + renderServerDropdown();
1829 + closeServerModal();
1830 + return;
1831 + }
1832 +
1833 + const duplicate = serverEntries.find(entry => entry.url === url && entry.type === type);
1834 + if (duplicate) {
1835 + const updatedDuplicate = {
1836 + id: duplicate.id,
1837 + url,
1838 + type,
1839 + bearerToken: sanitizedToken
1840 + };
1841 + serverEntries = [updatedDuplicate, ...serverEntries.filter(entry => entry.id !== duplicate.id)];
1842 + saveServerEntries(serverEntries);
1843 + persistSelectedServer(updatedDuplicate);
1844 + applyServerSelection(updatedDuplicate);
1845 + renderServerDropdown();
1846 + closeServerModal();
1847 + return;
1848 + }
1849 +
1850 + const newServer = {
1851 + id: generateServerId(),
1852 + url,
1853 + type,
1854 + bearerToken: sanitizedToken
1855 + };
1856 +
1857 + serverEntries = [newServer, ...serverEntries];
1858 + saveServerEntries(serverEntries);
1859 + persistSelectedServer(newServer);
1860 + applyServerSelection(newServer);
1861 + renderServerDropdown();
1862 + closeServerModal();
1863 + }
1864 +
1865 + jsonEditor.addEventListener('input', () => {
1866 + // Skip if this change was triggered by code (not user)
1867 + if (isUpdatingFromCode) return;
1868 +
1869 + // Debounce the update to avoid excessive updates while typing
1870 + clearTimeout(jsonEditorChangeTimer);
1871 + jsonEditorChangeTimer = setTimeout(() => {
1872 + // Only update form if we're on the edit tab and have a schema
1873 + if (currentActiveTab === 'editRequest' && schemaFormGenerator) {
1874 + updateFormFromRaw();
1875 + }
1876 + }, 500); // 500ms delay
1877 + });
1878 +
1879 + // Flow selection
1880 + flowsList.addEventListener('click', (e) => {
1881 + const flowItem = e.target.closest('.flow-item');
1882 + if (flowItem) {
1883 + document.querySelectorAll('.flow-item').forEach(item => item.classList.remove('active'));
1884 + flowItem.classList.add('active');
1885 + const flowName = flowItem.dataset.flow;
1886 + displayMethods(flowName);
1887 + }
1888 + });
1889 +
1890 + // Initialize from storage
1891 + function initializeServerState() {
1892 + serverEntries = loadServerEntries();
1893 + selectedServerId = loadSelectedServerKey();
1894 +
1895 + if (!serverEntries.length) {
1896 + const legacyUrl = loadServerUrl();
1897 + const fallbackUrl = typeof legacyUrl === 'string' && legacyUrl ? legacyUrl : DEFAULT_WS_URL;
1898 + const inferredType = isHttpUrl(fallbackUrl)
1899 + ? (loadHttpTransportPreference() === 'sse' ? 'sse' : 'stream-http')
1900 + : 'websocket';
1901 + const defaultServer = {
1902 + id: generateServerId(),
1903 + url: fallbackUrl,
1904 + type: inferredType,
1905 + bearerToken: ''
1906 + };
1907 + serverEntries = [defaultServer];
1908 + saveServerEntries(serverEntries);
1909 + selectedServerId = defaultServer.id;
1910 + saveSelectedServerKey(selectedServerId);
1911 + }
1912 +
1913 + if (selectedServerId && !getServerById(selectedServerId)) {
1914 + selectedServerId = serverEntries[0]?.id || null;
1915 + saveSelectedServerKey(selectedServerId);
1916 + } else if (!selectedServerId && serverEntries.length) {
1917 + selectedServerId = serverEntries[0].id;
1918 + saveSelectedServerKey(selectedServerId);
1919 + }
1920 +
1921 + renderServerDropdown();
1922 + const activeServer = getServerById(selectedServerId);
1923 + if (activeServer) {
1924 + persistSelectedServer(activeServer);
1925 + }
1926 + applyServerSelection(activeServer);
1927 + }
1928 +
1929 + function initializeFromStorage() {
1930 + initializeServerState();
1931 +
1932 + // Load request history
1933 + requestHistory = loadRequestHistory();
1934 +
1935 + // Update history flow indicator
1936 + updateHistoryFlowIndicator();
1937 + }
1938 +
1939 + function isWebSocketUrl(url) {
1940 + return url.startsWith('ws://') || url.startsWith('wss://');
1941 + }
1942 +
1943 + function isHttpUrl(url) {
1944 + return url.startsWith('http://') || url.startsWith('https://');
1945 + }
1946 +
1947 + function determineTransportForUrl(url) {
1948 + if (isWebSocketUrl(url)) {
1949 + return 'websocket';
1950 + }
1951 + if (isHttpUrl(url)) {
1952 + if (url.includes('transport=sse')) {
1953 + httpTransportPreference = 'sse';
1954 + httpTransportSelect.value = 'sse';
1955 + saveHttpTransportPreference('sse');
1956 + return 'sse';
1957 + }
1958 + return httpTransportPreference || 'stream-http';
1959 + }
1960 + return 'websocket';
1961 + }
1962 +
1963 + function getAuthorizationHeader() {
1964 + const server = getSelectedServer();
1965 + if (!server) {
1966 + return null;
1967 + }
1968 + if (!server.bearerToken) {
1969 + return null;
1970 + }
1971 + if (server.type !== 'stream-http' && server.type !== 'sse') {
1972 + return null;
1973 + }
1974 + return 'Bearer ' + server.bearerToken;
1975 + }
1976 +
1977 + // Update history flow indicator
1978 + function updateHistoryFlowIndicator() {
1979 + const historyCount = loadRequestHistory().length;
1980 + const historyFlow = document.querySelector('[data-flow="custom"]');
1981 +
1982 + if (historyCount > 0) {
1983 + historyFlow.textContent = `History (${historyCount})`;
1984 + historyFlow.style.fontWeight = 'bold';
1985 + historyFlow.style.color = '#0088cc';
1986 + } else {
1987 + historyFlow.textContent = 'History';
1988 + historyFlow.style.fontWeight = '';
1989 + historyFlow.style.color = '';
1990 + }
1991 + }
1992 +
1993 + // Initialize JSON pretty printer
1994 + jsonPrinter = new JSONPrettyPrinter({
1995 + indent: 2,
1996 + visualizeNewlines: true,
1997 + detectNestedJSON: true,
1998 + syntaxHighlight: true
1999 + });
2000 +
2001 + // Initialize with saved data
2002 + initializeFromStorage();
2003 +
2004 + // Initialize with first flow
2005 + displayMethods('initialization');
2006 + updateNavButtons();
2007 +
2008 + // Keyboard shortcuts for response navigation
2009 + document.addEventListener('keydown', (e) => {
2010 + // Only work when not focused on an input field
2011 + if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') {
2012 + return;
2013 + }
2014 +
2015 if (e.key === 'ArrowUp' && e.ctrlKey) {
2016 e.preventDefault();
2017 navigateToPrevResponse();
@@ -1652,224 +2442,161 @@
2442
2443 // Connect and Handshake automation
2444 async function connectAndInitialize() {
1655 - const url = serverUrlInput.value;
2445 + const server = getSelectedServer();
2446 + const url = server ? server.url.trim() : '';
2447 if (!url) {
1657 - alert('Please enter a WebSocket URL');
2448 + alert('Please add and select a server before connecting.');
2449 return;
2450 }
2451
2452 try {
2453 log('Starting automated connection and handshake...');
1663 -
1664 - // Step 1: Connect
1665 - await new Promise((resolve, reject) => {
1666 - log('1. Connecting to ' + url + '...');
1667 -
1668 - // Clear cached data from previous connection
1669 - availableTools = {};
1670 - availablePrompts = {};
1671 - availableResources = {};
1672 -
1673 - ws = new WebSocket(url);
1674 -
1675 - ws.onopen = () => {
1676 - log('✓ Connected to ' + url);
1677 - updateStatus(true);
1678 - saveServerUrl(url);
1679 - resolve();
1680 - };
1681 -
1682 - ws.onerror = (error) => {
1683 - log('✗ WebSocket connection error: ' + error);
1684 - reject(new Error('Connection failed'));
1685 - };
1686 -
1687 - ws.onclose = () => {
1688 - if (ws.readyState !== WebSocket.OPEN) {
1689 - log('✗ Connection closed before opening');
1690 - reject(new Error('Connection closed'));
1691 - }
1692 - };
1693 -
1694 - // Set up message handler for responses
1695 - ws.onmessage = (event) => {
1696 - try {
1697 - const data = JSON.parse(event.data);
1698 -
1699 - // Calculate metrics
1700 - let responseTime = null;
1701 - if (data.id !== undefined && pendingRequests.has(data.id)) {
1702 - responseTime = Date.now() - pendingRequests.get(data.id);
1703 - pendingRequests.delete(data.id);
1704 - }
1705 -
1706 - const responseSize = new Blob([event.data]).size;
1707 - const estimatedTokens = estimateTokens(event.data);
1708 -
1709 - logMessage(data, 'received', {
1710 - responseTime,
1711 - responseSize,
1712 - estimatedTokens
1713 - });
1714 -
1715 - // Handle specific responses
1716 - if (data.id && data.result) {
1717 - handleResponse(data);
1718 - }
1719 - } catch (e) {
1720 - log('← Received (raw): ' + event.data);
1721 - }
1722 - };
1723 - });
1724 -
1725 - // Step 2: Send initialize
1726 - await new Promise((resolve, reject) => {
1727 - log('2. Sending initialize request...');
1728 -
1729 - const initRequest = {
1730 - jsonrpc: "2.0",
1731 - id: currentRequestId++,
1732 - method: "initialize",
1733 - params: {
1734 - protocolVersion: "2024-11-05",
1735 - capabilities: {
1736 - roots: { listChanged: true },
1737 - sampling: {}
1738 - },
1739 - clientInfo: {
1740 - name: "Netdata MCP Test Client",
1741 - version: "1.0.0"
1742 - }
1743 - }
1744 - };
1745 -
1746 - // Track this request
1747 - pendingRequests.set(initRequest.id, Date.now());
1748 -
1749 - // Set up one-time response handler
1750 - const originalHandler = ws.onmessage;
1751 - let responseReceived = false;
1752 -
1753 - const timeout = setTimeout(() => {
1754 - if (!responseReceived) {
1755 - ws.onmessage = originalHandler;
1756 - reject(new Error('Initialize request timed out'));
1757 - }
1758 - }, 10000); // 10 second timeout
1759 -
1760 - ws.onmessage = (event) => {
1761 - // Call original handler first
1762 - originalHandler(event);
1763 -
1764 - if (responseReceived) return;
1765 -
1766 - try {
1767 - const data = JSON.parse(event.data);
1768 - if (data.id === initRequest.id) {
1769 - responseReceived = true;
1770 - clearTimeout(timeout);
1771 - ws.onmessage = originalHandler;
1772 -
1773 - if (data.error) {
1774 - log('✗ Initialize failed: ' + JSON.stringify(data.error));
1775 - reject(new Error('Initialize failed: ' + data.error.message));
1776 - } else {
1777 - log('✓ Initialize successful');
1778 - resolve();
1779 - }
1780 - }
1781 - } catch (e) {
1782 - // Ignore parsing errors for this handler
2454 +
2455 + await connect();
2456 +
2457 + const transportLabel = currentTransport === 'websocket'
2458 + ? 'WebSocket'
2459 + : currentTransport === 'sse'
2460 + ? 'SSE'
2461 + : 'streamable HTTP';
2462 + log('✓ Connected using ' + transportLabel);
2463 +
2464 + // Step 2: initialize request
2465 + log('2. Sending initialize request...');
2466 + const initRequest = {
2467 + jsonrpc: '2.0',
2468 + id: currentRequestId++,
2469 + method: 'initialize',
2470 + params: {
2471 + protocolVersion: '2024-11-05',
2472 + capabilities: {
2473 + roots: { listChanged: true },
2474 + sampling: {}
2475 + },
2476 + clientInfo: {
2477 + name: 'Netdata MCP Test Client',
2478 + version: '1.0.0'
2479 }
1784 - };
1785 -
1786 - ws.send(JSON.stringify(initRequest));
1787 - logMessage(initRequest, 'sent');
1788 - });
1789 -
1790 - // Step 3: Send initialized notification
2480 + }
2481 + };
2482 +
2483 + let initResponse;
2484 + try {
2485 + initResponse = await awaitWithTimeout(
2486 + dispatchRequest(initRequest, { awaitResponse: true }),
2487 + 10000,
2488 + 'Initialize request timed out'
2489 + );
2490 + } catch (err) {
2491 + pendingRequests.delete(initRequest.id);
2492 + throw err;
2493 + }
2494 +
2495 + if (initResponse && initResponse.error) {
2496 + throw new Error('Initialize failed: ' + initResponse.error.message);
2497 + }
2498 + log('✓ Initialize successful');
2499 +
2500 + // Step 3: Send initialized notification (no response expected)
2501 log('3. Sending initialized notification...');
2502 const initializedNotification = {
1793 - jsonrpc: "2.0",
1794 - method: "notifications/initialized"
2503 + jsonrpc: '2.0',
2504 + method: 'notifications/initialized'
2505 };
1796 -
1797 - ws.send(JSON.stringify(initializedNotification));
1798 - logMessage(initializedNotification, 'sent');
2506 + await dispatchRequest(initializedNotification, { awaitResponse: false });
2507 log('✓ Initialized notification sent');
1800 -
1801 - // Small delay to ensure notification is processed
1802 - await new Promise(resolve => setTimeout(resolve, 100));
1803 -
1804 - // Step 4: Send tools/list
1805 - await new Promise((resolve, reject) => {
1806 - log('4. Requesting tools list...');
1807 -
1808 - const toolsRequest = {
1809 - jsonrpc: "2.0",
1810 - id: currentRequestId++,
1811 - method: "tools/list"
1812 - };
1813 -
1814 - // Track this request
1815 - pendingRequests.set(toolsRequest.id, Date.now());
1816 -
1817 - // Set up one-time response handler
1818 - const originalHandler = ws.onmessage;
1819 - let responseReceived = false;
1820 -
1821 - const timeout = setTimeout(() => {
1822 - if (!responseReceived) {
1823 - ws.onmessage = originalHandler;
1824 - reject(new Error('Tools list request timed out'));
1825 - }
1826 - }, 10000); // 10 second timeout
1827 -
1828 - ws.onmessage = (event) => {
1829 - // Call original handler first
1830 - originalHandler(event);
1831 -
1832 - if (responseReceived) return;
1833 -
1834 - try {
1835 - const data = JSON.parse(event.data);
1836 - if (data.id === toolsRequest.id) {
1837 - responseReceived = true;
1838 - clearTimeout(timeout);
1839 - ws.onmessage = originalHandler;
1840 -
1841 - if (data.error) {
1842 - log('✗ Tools list failed: ' + JSON.stringify(data.error));
1843 - reject(new Error('Tools list failed: ' + data.error.message));
1844 - } else {
1845 - log('✓ Tools list received (' + (data.result.tools ? data.result.tools.length : 0) + ' tools)');
1846 - resolve();
1847 - }
1848 - }
1849 - } catch (e) {
1850 - // Ignore parsing errors for this handler
1851 - }
1852 - };
1853 -
1854 - ws.send(JSON.stringify(toolsRequest));
1855 - logMessage(toolsRequest, 'sent');
1856 - });
1857 -
1858 - // Step 5: Set up proper persistent event handlers
1859 - log('5. Setting up persistent connection handlers...');
1860 - ws.onerror = (error) => {
1861 - log('WebSocket error: ' + error);
2508 +
2509 + await delay(100);
2510 +
2511 + // Step 4: Request tools list
2512 + log('4. Requesting tools list...');
2513 + const toolsRequest = {
2514 + jsonrpc: '2.0',
2515 + id: currentRequestId++,
2516 + method: 'tools/list'
2517 };
1863 -
1864 - ws.onclose = () => {
1865 - log('Disconnected');
1866 - updateStatus(false);
1867 - ws = null;
2518 +
2519 + let toolsResponse;
2520 + try {
2521 + toolsResponse = await awaitWithTimeout(
2522 + dispatchRequest(toolsRequest, { awaitResponse: true }),
2523 + 10000,
2524 + 'Tools list request timed out'
2525 + );
2526 + } catch (err) {
2527 + pendingRequests.delete(toolsRequest.id);
2528 + throw err;
2529 + }
2530 +
2531 + if (toolsResponse && toolsResponse.error) {
2532 + throw new Error('Tools list failed: ' + toolsResponse.error.message);
2533 + }
2534 +
2535 + const toolCount = toolsResponse && toolsResponse.result && toolsResponse.result.tools
2536 + ? toolsResponse.result.tools.length
2537 + : 0;
2538 + log('✓ Tools list received (' + toolCount + ' tools)');
2539 +
2540 + // Step 5: Request prompts list
2541 + log('5. Requesting prompts list...');
2542 + const promptsRequest = {
2543 + jsonrpc: '2.0',
2544 + id: currentRequestId++,
2545 + method: 'prompts/list'
2546 };
1869 - log('✓ Persistent handlers configured');
1870 -
1871 - // Step 6: Switch to Tools flow
1872 - log('6. Switching to Tools flow...');
2547 +
2548 + let promptsResponse;
2549 + try {
2550 + promptsResponse = await awaitWithTimeout(
2551 + dispatchRequest(promptsRequest, { awaitResponse: true }),
2552 + 10000,
2553 + 'Prompts list request timed out'
2554 + );
2555 + } catch (err) {
2556 + pendingRequests.delete(promptsRequest.id);
2557 + throw err;
2558 + }
2559 +
2560 + if (promptsResponse && promptsResponse.error) {
2561 + log('⚠ Prompts list failed: ' + JSON.stringify(promptsResponse.error));
2562 + } else {
2563 + const promptCount = promptsResponse && promptsResponse.result && promptsResponse.result.prompts
2564 + ? promptsResponse.result.prompts.length
2565 + : 0;
2566 + log('✓ Prompts list received (' + promptCount + ' prompts)');
2567 + }
2568 +
2569 + // Step 6: Request resources list
2570 + log('6. Requesting resources list...');
2571 + const resourcesRequest = {
2572 + jsonrpc: '2.0',
2573 + id: currentRequestId++,
2574 + method: 'resources/list'
2575 + };
2576 +
2577 + let resourcesResponse;
2578 + try {
2579 + resourcesResponse = await awaitWithTimeout(
2580 + dispatchRequest(resourcesRequest, { awaitResponse: true }),
2581 + 10000,
2582 + 'Resources list request timed out'
2583 + );
2584 + } catch (err) {
2585 + pendingRequests.delete(resourcesRequest.id);
2586 + throw err;
2587 + }
2588 +
2589 + if (resourcesResponse && resourcesResponse.error) {
2590 + log('⚠ Resources list failed: ' + JSON.stringify(resourcesResponse.error));
2591 + } else {
2592 + const resourceCount = resourcesResponse && resourcesResponse.result && resourcesResponse.result.resources
2593 + ? resourcesResponse.result.resources.length
2594 + : 0;
2595 + log('✓ Resources list received (' + resourceCount + ' resources)');
2596 + }
2597 +
2598 + // Step 7: Switch UI to tools flow
2599 + log('7. Switching to Tools flow...');
2600 document.querySelectorAll('.flow-item').forEach(item => item.classList.remove('active'));
2601 const toolsFlow = document.querySelector('[data-flow="tools"]');
2602 if (toolsFlow) {
@@ -1879,140 +2606,163 @@
2606 } else {
2607 log('⚠ Tools flow not found');
2608 }
1882 -
2609 +
2610 log('🎉 Automated connection and handshake completed successfully!');
1884 -
2611 } catch (error) {
2612 log('❌ Automated connection and handshake failed: ' + error.message);
1887 - if (ws) {
1888 - ws.close();
1889 - ws = null;
2613 + if (currentTransport === 'websocket') {
2614 + if (ws) {
2615 + ws.close();
2616 + ws = null;
2617 + }
2618 + } else {
2619 + if (activeSseController) {
2620 + activeSseController.abort();
2621 + activeSseController = null;
2622 + }
2623 + isConnected = false;
2624 updateStatus(false);
2625 }
2626 + pendingRequests.clear();
2627 }
2628 }
2629
1895 - // WebSocket functions
1896 - function connect() {
1897 - const url = serverUrlInput.value;
2630 + // Transport helpers
2631 + async function connect() {
2632 + const server = getSelectedServer();
2633 + const url = server ? server.url.trim() : '';
2634 if (!url) {
1899 - alert('Please enter a WebSocket URL');
2635 + alert('Please add and select a server before connecting.');
2636 return;
2637 }
1902 -
1903 - log('Connecting to ' + url + '...');
1904 -
2638 +
2639 + if (server && (server.type === 'stream-http' || server.type === 'sse')) {
2640 + httpTransportPreference = server.type;
2641 + httpTransportSelect.value = server.type;
2642 + saveHttpTransportPreference(server.type);
2643 + } else if (isHttpUrl(url)) {
2644 + httpTransportPreference = httpTransportSelect.value || httpTransportPreference || 'stream-http';
2645 + saveHttpTransportPreference(httpTransportPreference);
2646 + }
2647 +
2648 + currentTransport = determineTransportForUrl(url);
2649 + if (server && (server.type === 'stream-http' || server.type === 'sse')) {
2650 + currentTransport = server.type;
2651 + }
2652 +
2653 + currentServerUrl = url;
2654 +
2655 // Clear cached data from previous connection
2656 availableTools = {};
2657 availablePrompts = {};
2658 availableResources = {};
1909 -
1910 - ws = new WebSocket(url);
1911 -
1912 - ws.onopen = () => {
1913 - log('Connected to ' + url);
1914 - updateStatus(true);
1915 - // Save successful connection URL
1916 - saveServerUrl(url);
1917 - };
1918 -
1919 - ws.onmessage = (event) => {
1920 - try {
1921 - const data = JSON.parse(event.data);
1922 -
1923 - // Calculate metrics
1924 - let responseTime = null;
1925 - if (data.id !== undefined && pendingRequests.has(data.id)) {
1926 - responseTime = Date.now() - pendingRequests.get(data.id);
1927 - pendingRequests.delete(data.id);
2659 + pendingRequests.clear();
2660 +
2661 + if (currentTransport === 'websocket') {
2662 + await connectWebSocket(url);
2663 + } else {
2664 + await connectStateless(url);
2665 + }
2666 + }
2667 +
2668 + async function connectWebSocket(url) {
2669 + if (ws && ws.readyState === WebSocket.OPEN) {
2670 + ws.close();
2671 + }
2672 +
2673 + log('Connecting to ' + url + '...');
2674 +
2675 + await new Promise((resolve, reject) => {
2676 + let resolved = false;
2677 + ws = new WebSocket(url);
2678 +
2679 + ws.addEventListener('open', () => {
2680 + resolved = true;
2681 + isConnected = true;
2682 + saveServerUrl(url);
2683 + updateStatus(true);
2684 + log('Connected to ' + url);
2685 + resolve();
2686 + });
2687 +
2688 + ws.addEventListener('message', (event) => {
2689 + handleIncomingTransportPayload(event.data, { transport: 'websocket' });
2690 + });
2691 +
2692 + ws.addEventListener('error', (error) => {
2693 + log('WebSocket error: ' + error);
2694 + if (!resolved) {
2695 + reject(new Error('WebSocket connection error'));
2696 }
1929 -
1930 - const responseSize = new Blob([event.data]).size;
1931 - const estimatedTokens = estimateTokens(event.data);
1932 -
1933 - logMessage(data, 'received', {
1934 - responseTime,
1935 - responseSize,
1936 - estimatedTokens
1937 - });
1938 -
1939 - // Handle specific responses
1940 - if (data.id && data.result) {
1941 - handleResponse(data);
2697 + });
2698 +
2699 + ws.addEventListener('close', () => {
2700 + if (!resolved) {
2701 + reject(new Error('WebSocket connection closed before opening'));
2702 + } else {
2703 + log('Disconnected');
2704 }
1943 - } catch (e) {
1944 - log('← Received (raw): ' + event.data);
2705 + ws = null;
2706 + isConnected = false;
2707 + updateStatus(false);
2708 + });
2709 + });
2710 + }
2711 +
2712 + async function connectStateless(url) {
2713 + const mode = currentTransport === 'sse' ? 'SSE' : 'streamable HTTP';
2714 + log('Preparing ' + mode + ' session for ' + url + '...');
2715 + saveServerUrl(url);
2716 + if (activeSseController) {
2717 + activeSseController.abort();
2718 + }
2719 + activeSseController = null;
2720 + isConnected = true;
2721 + updateStatus(true);
2722 + log('Ready to send requests over ' + mode + '.');
2723 + }
2724 +
2725 + function disconnect() {
2726 + if (currentTransport === 'websocket') {
2727 + if (ws) {
2728 + ws.close();
2729 }
1946 - };
1947 -
1948 - ws.onerror = (error) => {
1949 - log('WebSocket error: ' + error);
1950 - };
1951 -
1952 - ws.onclose = () => {
1953 - log('Disconnected');
2730 + } else {
2731 + if (activeSseController) {
2732 + activeSseController.abort();
2733 + activeSseController = null;
2734 + }
2735 + if (isConnected) {
2736 + log('Disconnected');
2737 + }
2738 + isConnected = false;
2739 updateStatus(false);
1955 - ws = null;
1956 - };
2740 + }
2741 + pendingRequests.clear();
2742 }
1958 -
1959 - function disconnect() {
1960 - if (ws) {
1961 - ws.close();
2743 +
2744 + function ensureTransportConnected() {
2745 + if (currentTransport === 'websocket') {
2746 + return ws && ws.readyState === WebSocket.OPEN;
2747 }
2748 + return isConnected;
2749 }
1964 -
1965 - function sendRequest() {
1966 - if (!ws || ws.readyState !== WebSocket.OPEN) {
2750 +
2751 + async function sendRequest() {
2752 + if (!ensureTransportConnected()) {
2753 alert('Not connected to server');
2754 return;
2755 }
1970 -
2756 +
2757 + let request;
2758 try {
1972 - const request = JSON.parse(jsonEditor.value);
1973 -
1974 - // Track request timestamp if it has an ID
1975 - if (request.id !== undefined) {
1976 - pendingRequests.set(request.id, Date.now());
1977 - }
1978 -
1979 - // Determine tool name if it's a tools/call request
1980 - let toolName = null;
1981 - if (request.method === 'tools/call' && request.params && request.params.name) {
1982 - toolName = request.params.name;
1983 - }
1984 -
1985 - // Save to history
1986 - const historyItem = saveRequestToHistory(request, request.method, toolName, false);
1987 -
1988 - // Update in-memory history for immediate UI updates
1989 - const oldHistoryItem = {
1990 - timestamp: new Date().toLocaleTimeString(),
1991 - method: request.method,
1992 - request: JSON.parse(JSON.stringify(request)), // Deep clone here too
1993 - error: false
1994 - };
1995 - requestHistory.unshift(oldHistoryItem);
1996 - if (requestHistory.length > 50) {
1997 - requestHistory = requestHistory.slice(0, 50);
1998 - }
1999 -
2000 - // Update history flow indicator
2001 - updateHistoryFlowIndicator();
2002 -
2003 - ws.send(JSON.stringify(request));
2004 - logMessage(request, 'sent');
2005 -
2006 - // Increment ID after sending for next request
2007 - if (request.id !== undefined && request.id === currentRequestId) {
2008 - currentRequestId++;
2009 - }
2759 + request = JSON.parse(jsonEditor.value);
2760 } catch (e) {
2761 alert('Invalid JSON: ' + e.message);
2012 -
2762 +
2763 // Save error to history
2764 saveRequestToHistory({ error: e.message }, 'Invalid JSON', null, true);
2015 -
2765 +
2766 // Add error to in-memory history
2767 const historyItem = {
2768 timestamp: new Date().toLocaleTimeString(),
@@ -2021,9 +2771,431 @@
2771 error: true
2772 };
2773 requestHistory.unshift(historyItem);
2024 -
2025 - // Update history flow indicator
2774 updateHistoryFlowIndicator();
2775 + return;
2776 + }
2777 +
2778 + // Determine tool name if it's a tools/call request
2779 + let toolName = null;
2780 + if (request.method === 'tools/call' && request.params && request.params.name) {
2781 + toolName = request.params.name;
2782 + }
2783 +
2784 + // Save to history
2785 + saveRequestToHistory(request, request.method, toolName, false);
2786 +
2787 + // Update in-memory history for immediate UI updates
2788 + const historyItem = {
2789 + timestamp: new Date().toLocaleTimeString(),
2790 + method: request.method,
2791 + request: JSON.parse(JSON.stringify(request)),
2792 + error: false
2793 + };
2794 + requestHistory.unshift(historyItem);
2795 + if (requestHistory.length > 50) {
2796 + requestHistory = requestHistory.slice(0, 50);
2797 + }
2798 + updateHistoryFlowIndicator();
2799 +
2800 + try {
2801 + const shouldAwaitResponse = currentTransport !== 'websocket';
2802 + await dispatchRequest(request, { awaitResponse: shouldAwaitResponse });
2803 + } catch (err) {
2804 + log('Request failed: ' + err.message);
2805 + }
2806 +
2807 + // Increment ID after sending for next request
2808 + if (request.id !== undefined && request.id === currentRequestId) {
2809 + currentRequestId++;
2810 + }
2811 + }
2812 +
2813 + async function dispatchRequest(request, options = {}) {
2814 + const awaitResponse = options.awaitResponse !== false && request.id !== undefined;
2815 + const payloadText = JSON.stringify(request);
2816 + let tracker = null;
2817 +
2818 + if (request.id !== undefined) {
2819 + tracker = registerPendingRequest(request.id, { createPromise: awaitResponse });
2820 + }
2821 +
2822 + try {
2823 + if (currentTransport === 'websocket') {
2824 + if (!ws || ws.readyState !== WebSocket.OPEN) {
2825 + throw new Error('WebSocket not connected');
2826 + }
2827 + logMessage(request, 'sent');
2828 + ws.send(payloadText);
2829 + } else if (currentTransport === 'stream-http') {
2830 + logMessage(request, 'sent');
2831 + await sendOverHttp(request, payloadText, awaitResponse);
2832 + } else if (currentTransport === 'sse') {
2833 + logMessage(request, 'sent');
2834 + await sendOverSse(request, payloadText, awaitResponse);
2835 + } else {
2836 + throw new Error('Unsupported transport: ' + currentTransport);
2837 + }
2838 + } catch (error) {
2839 + if (request.id !== undefined) {
2840 + pendingRequests.delete(request.id);
2841 + if (tracker && tracker.reject) {
2842 + tracker.reject(error);
2843 + }
2844 + }
2845 + throw error;
2846 + }
2847 +
2848 + if (awaitResponse && tracker && tracker.promise) {
2849 + return tracker.promise;
2850 + }
2851 + return null;
2852 + }
2853 +
2854 + function registerPendingRequest(id, { createPromise = false } = {}) {
2855 + if (id === undefined || id === null) {
2856 + return null;
2857 + }
2858 +
2859 + const entry = {
2860 + timestamp: Date.now()
2861 + };
2862 +
2863 + if (createPromise) {
2864 + entry.promise = new Promise((resolve, reject) => {
2865 + entry.resolve = resolve;
2866 + entry.reject = reject;
2867 + });
2868 + }
2869 +
2870 + pendingRequests.set(id, entry);
2871 + return entry;
2872 + }
2873 +
2874 + async function awaitWithTimeout(promise, timeoutMs, errorMessage) {
2875 + if (!timeoutMs) {
2876 + return promise;
2877 + }
2878 +
2879 + let timeoutId;
2880 + try {
2881 + return await Promise.race([
2882 + promise,
2883 + new Promise((_, reject) => {
2884 + timeoutId = setTimeout(() => reject(new Error(errorMessage)), timeoutMs);
2885 + })
2886 + ]);
2887 + } finally {
2888 + if (timeoutId) {
2889 + clearTimeout(timeoutId);
2890 + }
2891 + }
2892 + }
2893 +
2894 + function delay(ms) {
2895 + return new Promise(resolve => setTimeout(resolve, ms));
2896 + }
2897 +
2898 + async function sendOverHttp(request, payloadText, expectResponse = true) {
2899 + let response;
2900 + try {
2901 + const headers = {
2902 + 'Content-Type': 'application/json',
2903 + 'Accept': 'application/json'
2904 + };
2905 + const authHeader = getAuthorizationHeader();
2906 + if (authHeader) {
2907 + headers.Authorization = authHeader;
2908 + }
2909 +
2910 + response = await fetch(currentServerUrl, {
2911 + method: 'POST',
2912 + headers,
2913 + body: payloadText
2914 + });
2915 + } catch (error) {
2916 + if (!expectResponse) {
2917 + log('⚠ Streamable HTTP request completed with network error (no response expected): ' + error.message);
2918 + return;
2919 + }
2920 + throw error;
2921 + }
2922 +
2923 + let responseText = '';
2924 + try {
2925 + if (expectResponse) {
2926 + responseText = await response.text();
2927 + } else {
2928 + // Drain body if any but ignore errors
2929 + if (response.body) {
2930 + await response.body.cancel().catch(() => {});
2931 + }
2932 + }
2933 + } catch (error) {
2934 + if (!expectResponse) {
2935 + log('⚠ Streamable HTTP response could not be fully read (no response expected): ' + error.message);
2936 + return;
2937 + }
2938 + throw error;
2939 + }
2940 +
2941 + if (!response.ok) {
2942 + if (responseText) {
2943 + handleIncomingTransportPayload(responseText, {
2944 + transport: 'stream-http',
2945 + responseSize: new Blob([responseText]).size
2946 + });
2947 + }
2948 + throw new Error('HTTP ' + response.status + ' ' + response.statusText);
2949 + }
2950 +
2951 + if (!expectResponse) {
2952 + return;
2953 + }
2954 +
2955 + if (responseText) {
2956 + handleIncomingTransportPayload(responseText, {
2957 + transport: 'stream-http',
2958 + responseSize: new Blob([responseText]).size
2959 + });
2960 + } else {
2961 + log('← Received empty response with status ' + response.status + ' (stream-http)');
2962 + }
2963 + }
2964 +
2965 + async function sendOverSse(request, payloadText, expectResponse = true) {
2966 + if (activeSseController) {
2967 + activeSseController.abort();
2968 + }
2969 +
2970 + activeSseController = new AbortController();
2971 +
2972 + let response;
2973 + try {
2974 + const headers = {
2975 + 'Content-Type': 'application/json',
2976 + 'Accept': 'text/event-stream'
2977 + };
2978 + const authHeader = getAuthorizationHeader();
2979 + if (authHeader) {
2980 + headers.Authorization = authHeader;
2981 + }
2982 +
2983 + response = await fetch(enhanceUrlForSse(currentServerUrl), {
2984 + method: 'POST',
2985 + headers,
2986 + body: payloadText,
2987 + signal: activeSseController.signal
2988 + });
2989 + } catch (error) {
2990 + activeSseController = null;
2991 + if (!expectResponse) {
2992 + log('⚠ SSE request completed with network error (no response expected): ' + error.message);
2993 + return;
2994 + }
2995 + throw error;
2996 + }
2997 +
2998 + if (!response.ok) {
2999 + const errorBody = await response.text().catch(() => '');
3000 + if (errorBody) {
3001 + handleIncomingTransportPayload(errorBody, {
3002 + transport: 'sse',
3003 + responseSize: new Blob([errorBody]).size
3004 + });
3005 + }
3006 + throw new Error('HTTP ' + response.status + ' ' + response.statusText);
3007 + }
3008 +
3009 + if (!expectResponse) {
3010 + activeSseController = null;
3011 + return;
3012 + }
3013 +
3014 + if (!response.body) {
3015 + throw new Error('SSE response has no body');
3016 + }
3017 +
3018 + const reader = response.body.getReader();
3019 + const decoder = new TextDecoder();
3020 + let buffer = '';
3021 +
3022 + try {
3023 + while (true) {
3024 + const { value, done } = await reader.read();
3025 + if (done) {
3026 + break;
3027 + }
3028 +
3029 + buffer += decoder.decode(value, { stream: true });
3030 +
3031 + let normalized = buffer.replace(/\r\n/g, '\n');
3032 + let lastProcessedIndex = 0;
3033 + let separatorIndex;
3034 +
3035 + while ((separatorIndex = normalized.indexOf('\n\n', lastProcessedIndex)) !== -1) {
3036 + const rawEvent = normalized.slice(lastProcessedIndex, separatorIndex);
3037 + lastProcessedIndex = separatorIndex + 2;
3038 +
3039 + const parsed = parseSseEvent(rawEvent);
3040 + if (!parsed) {
3041 + continue;
3042 + }
3043 +
3044 + const { event, data } = parsed;
3045 + if (!data) {
3046 + continue;
3047 + }
3048 +
3049 + try {
3050 + const jsonData = JSON.parse(data);
3051 + const rawJson = JSON.stringify(jsonData);
3052 + handleIncomingTransportPayload(jsonData, {
3053 + transport: 'sse',
3054 + responseSize: new Blob([rawJson]).size
3055 + });
3056 +
3057 + if (event && event.toLowerCase() === 'complete') {
3058 + buffer = normalized.slice(lastProcessedIndex);
3059 + return;
3060 + }
3061 + } catch (error) {
3062 + log('Failed to parse SSE data: ' + error.message + ' (' + data + ')');
3063 + }
3064 + }
3065 +
3066 + buffer = normalized.slice(lastProcessedIndex);
3067 + }
3068 + } finally {
3069 + activeSseController = null;
3070 + }
3071 + }
3072 +
3073 + function enhanceUrlForSse(url) {
3074 + if (url.includes('transport=sse')) {
3075 + return url;
3076 + }
3077 + const hasQuery = url.includes('?');
3078 + return url + (hasQuery ? '&' : '?') + 'transport=sse';
3079 + }
3080 +
3081 + function parseSseEvent(rawEvent) {
3082 + const lines = rawEvent.split('\n');
3083 + const result = { event: 'message', data: '' };
3084 +
3085 + for (const line of lines) {
3086 + if (line.startsWith('event:')) {
3087 + result.event = line.slice(6).trim();
3088 + } else if (line.startsWith('data:')) {
3089 + let value = line.slice(5);
3090 + if (value.startsWith(' ')) {
3091 + value = value.slice(1);
3092 + }
3093 + value = value.replace(/\r/g, '');
3094 + result.data += result.data ? '\n' + value : value;
3095 + }
3096 + }
3097 +
3098 + result.data = result.data.trim();
3099 + return result.data ? result : null;
3100 + }
3101 +
3102 + function handleIncomingTransportPayload(payload, meta = {}) {
3103 + if (payload === null || payload === undefined) {
3104 + return;
3105 + }
3106 +
3107 + if (typeof payload === 'string') {
3108 + const trimmed = payload.trim();
3109 + if (!trimmed) {
3110 + return;
3111 + }
3112 +
3113 + try {
3114 + const parsed = JSON.parse(trimmed);
3115 + dispatchParsedPayload(parsed, trimmed, meta);
3116 + } catch (error) {
3117 + const asLines = trimmed.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
3118 + const parsedMessages = [];
3119 + let ndjsonValid = asLines.length > 1;
3120 +
3121 + if (ndjsonValid) {
3122 + for (const line of asLines) {
3123 + try {
3124 + parsedMessages.push(JSON.parse(line));
3125 + } catch (parseError) {
3126 + ndjsonValid = false;
3127 + break;
3128 + }
3129 + }
3130 + }
3131 +
3132 + if (ndjsonValid && parsedMessages.length) {
3133 + parsedMessages.forEach(item => {
3134 + const raw = JSON.stringify(item);
3135 + dispatchParsedPayload(item, raw, meta);
3136 + });
3137 + } else {
3138 + log('← Received (raw): ' + payload);
3139 + }
3140 + }
3141 + return;
3142 + }
3143 +
3144 + if (Array.isArray(payload)) {
3145 + payload.forEach(item => {
3146 + const raw = JSON.stringify(item);
3147 + dispatchParsedPayload(item, raw, meta);
3148 + });
3149 + } else if (typeof payload === 'object') {
3150 + const raw = JSON.stringify(payload);
3151 + dispatchParsedPayload(payload, raw, meta);
3152 + }
3153 + }
3154 +
3155 + function dispatchParsedPayload(parsed, raw, meta) {
3156 + if (Array.isArray(parsed)) {
3157 + parsed.forEach(item => {
3158 + const rawItem = JSON.stringify(item);
3159 + processJsonRpcMessage(item, rawItem, meta);
3160 + });
3161 + } else {
3162 + processJsonRpcMessage(parsed, raw, meta);
3163 + }
3164 + }
3165 +
3166 + function processJsonRpcMessage(obj, rawText, meta = {}) {
3167 + if (!obj) {
3168 + return;
3169 + }
3170 +
3171 + const text = rawText || JSON.stringify(obj);
3172 + const tracker = obj.id !== undefined ? pendingRequests.get(obj.id) : null;
3173 +
3174 + let responseTime = null;
3175 + if (meta.responseTime !== undefined) {
3176 + responseTime = meta.responseTime;
3177 + } else if (tracker) {
3178 + responseTime = Date.now() - tracker.timestamp;
3179 + }
3180 +
3181 + const responseSize = meta.responseSize !== undefined ? meta.responseSize : new Blob([text]).size;
3182 + const estimatedTokens = meta.estimatedTokens !== undefined ? meta.estimatedTokens : estimateTokens(text);
3183 +
3184 + if (tracker) {
3185 + pendingRequests.delete(obj.id);
3186 + if (tracker.resolve) {
3187 + tracker.resolve(obj);
3188 + }
3189 + }
3190 +
3191 + logMessage(obj, 'received', {
3192 + responseTime,
3193 + responseSize,
3194 + estimatedTokens
3195 + });
3196 +
3197 + if (obj.result) {
3198 + handleResponse(obj);
3199 }
3200 }
3201
@@ -2431,4 +3603,4 @@
3603 }
3604 </script>
3605 </body>
2434 -</html>
\ No newline at end of file
3606 +</html>
src/web/mcp/mcp-tools-alert-transitions.c
+3 -3
@@ -155,8 +155,8 @@ void mcp_tool_list_alert_transitions_schema(BUFFER *buffer) {
155 }
156
157 // Execute alert transitions query
158 -MCP_RETURN_CODE mcp_tool_list_alert_transitions_execute(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) {
159 - if (!mcpc || id == 0)
158 +MCP_RETURN_CODE mcp_tool_list_alert_transitions_execute(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id __maybe_unused) {
159 + if (!mcpc)
160 return MCP_RC_ERROR;
161
162 // Extract nodes array
@@ -350,4 +350,4 @@ MCP_RETURN_CODE mcp_tool_list_alert_transitions_execute(MCP_CLIENT *mcpc, struct
350 buffer_json_finalize(mcpc->result); // Finalize the JSON
351
352 return MCP_RC_OK;
353 -}
\ No newline at end of file
353 +}
src/web/mcp/mcp-tools-configured-alerts.c
+3 -3
@@ -19,8 +19,8 @@ void mcp_tool_list_configured_alerts_schema(BUFFER *buffer) {
19 }
20
21 // Execute list_configured_alerts - no filtering, returns all prototypes
22 -MCP_RETURN_CODE mcp_tool_list_configured_alerts_execute(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, MCP_REQUEST_ID id) {
23 - if (!mcpc || id == 0)
22 +MCP_RETURN_CODE mcp_tool_list_configured_alerts_execute(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, MCP_REQUEST_ID id __maybe_unused) {
23 + if (!mcpc)
24 return MCP_RC_ERROR;
25
26 // Create a temporary buffer for the result
@@ -119,4 +119,4 @@ MCP_RETURN_CODE mcp_tool_list_configured_alerts_execute(MCP_CLIENT *mcpc, struct
119 buffer_json_finalize(mcpc->result); // Finalize the JSON
120
121 return MCP_RC_OK;
122 -}
\ No newline at end of file
122 +}
src/web/mcp/mcp-tools-execute-function.c
+8 -3
@@ -1274,7 +1274,12 @@ static void mcp_process_table_result(MCP_FUNCTION_DATA *data, size_t max_size_th
1274
1275 // Apply row limit
1276 size_t limit = row_idx;
1277 - if (data->request.limit > 0 && data->request.limit < limit && data->input.type == FN_TYPE_TABLE) {
1277 + bool force_limit = (data->output.status == MCP_TABLE_RESPONSE_TOO_BIG && data->request.limit > 0);
1278 +
1279 + if (force_limit && data->request.limit < limit) {
1280 + limit = data->request.limit;
1281 + }
1282 + else if (!force_limit && data->request.limit > 0 && data->request.limit < limit && data->input.type == FN_TYPE_TABLE) {
1283 // we don't limit history functions, only regular tables
1284 // for history functions, we sent the limit to the backend
1285 // so whatever it returns is what we show
@@ -2596,9 +2601,9 @@ static bool check_requirements_and_violations(MCP_FUNCTION_DATA *data,
2601 return false;
2602 }
2603
2599 -MCP_RETURN_CODE mcp_tool_execute_function_execute(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id)
2604 +MCP_RETURN_CODE mcp_tool_execute_function_execute(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id __maybe_unused)
2605 {
2601 - if (!mcpc || id == 0 || !params)
2606 + if (!mcpc || !params)
2607 return MCP_RC_ERROR;
2608
2609 // Create and initialize function data structure
src/web/mcp/mcp-tools-list-metadata.c
+3 -3
@@ -387,9 +387,9 @@ void mcp_unified_list_tool_schema(BUFFER *buffer, const MCP_LIST_TOOL_CONFIG *co
387
388 // Unified execution
389 MCP_RETURN_CODE mcp_unified_list_tool_execute(MCP_CLIENT *mcpc, const MCP_LIST_TOOL_CONFIG *config,
390 - struct json_object *params, MCP_REQUEST_ID id)
390 + struct json_object *params, MCP_REQUEST_ID id __maybe_unused)
391 {
392 - if (!mcpc || !config || id == 0)
392 + if (!mcpc || !config)
393 return MCP_RC_ERROR;
394
395 // Extract parameters based on configuration
@@ -532,4 +532,4 @@ MCP_RETURN_CODE mcp_unified_list_tool_execute(MCP_CLIENT *mcpc, const MCP_LIST_T
532 buffer_json_finalize(mcpc->result); // Finalize the JSON
533
534 return MCP_RC_OK;
535 -}
\ No newline at end of file
535 +}
src/web/mcp/mcp-tools-query-metrics.c
+3 -6
@@ -293,12 +293,10 @@ static bool mcp_query_interrupt_callback(void *data) {
293 // Removed extract_string_param and extract_size_param - now using mcp-params functions
294
295 // Execute the metrics query
296 -MCP_RETURN_CODE mcp_tool_query_metrics_execute(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) {
297 - if (!mcpc || id == 0)
296 +MCP_RETURN_CODE mcp_tool_query_metrics_execute(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id __maybe_unused) {
297 + if (!mcpc)
298 return MCP_RC_ERROR;
299
300 - buffer_flush(mcpc->result);
301 -
300 usec_t received_ut = now_monotonic_usec();
301
302 // Extract and validate context parameter
@@ -592,7 +590,6 @@ MCP_RETURN_CODE mcp_tool_query_metrics_execute(MCP_CLIENT *mcpc, struct json_obj
590 onewayalloc_destroy(owa);
591
592 if (ret != HTTP_RESP_OK) {
595 - buffer_flush(mcpc->result);
593 const char *error_desc = "unknown error";
594
595 // Map common HTTP error codes to more descriptive messages
@@ -693,4 +690,4 @@ MCP_RETURN_CODE mcp_tool_query_metrics_execute(MCP_CLIENT *mcpc, struct json_obj
690 buffer_json_finalize(mcpc->result); // Finalize the JSON
691
692 return MCP_RC_OK;
696 -}
\ No newline at end of file
693 +}
src/web/mcp/mcp-tools.c
+4 -8
@@ -269,8 +269,8 @@ static const MCP_TOOL_DEF mcp_tools[] = {
269 };
270
271 // Return a list of available tools
272 -static MCP_RETURN_CODE mcp_tools_method_list(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, MCP_REQUEST_ID id) {
273 - if (!mcpc || id == 0)
272 +static MCP_RETURN_CODE mcp_tools_method_list(MCP_CLIENT *mcpc, struct json_object *params __maybe_unused, MCP_REQUEST_ID id __maybe_unused) {
273 + if (!mcpc)
274 return MCP_RC_ERROR;
275
276 // Initialize success response
@@ -312,8 +312,8 @@ static MCP_RETURN_CODE mcp_tools_method_list(MCP_CLIENT *mcpc, struct json_objec
312 }
313
314 // Main execute method that routes to specific tool handlers
315 -static MCP_RETURN_CODE mcp_tools_method_call(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id) {
316 - if (!mcpc || !params || id == 0)
315 +static MCP_RETURN_CODE mcp_tools_method_call(MCP_CLIENT *mcpc, struct json_object *params, MCP_REQUEST_ID id __maybe_unused) {
316 + if (!mcpc || !params)
317 return MCP_RC_ERROR;
318
319 // Extract tool name
@@ -356,10 +356,6 @@ MCP_RETURN_CODE mcp_tools_route(MCP_CLIENT *mcpc, const char *method, struct jso
356
357 netdata_log_debug(D_MCP, "MCP tools method: %s", method);
358
359 - // Flush previous buffers
360 - buffer_flush(mcpc->result);
361 - buffer_flush(mcpc->error);
362 -
359 MCP_RETURN_CODE rc;
360
361 if (strcmp(method, "list") == 0) {
src/web/mcp/mcp.c
+215 -297
@@ -9,8 +9,9 @@
9 #include "mcp-logging.h"
10 #include "mcp-completion.h"
11 #include "mcp-tools-execute-function-registry.h"
12 -#include "adapters/mcp-websocket.h"
13 -#include "mcp-api-key.h"
12 +#include "web/api/mcp_auth.h"
13 +
14 +static bool mcp_initialized = false;
15
16 // Define the enum to string mapping for protocol versions
17 ENUM_STR_MAP_DEFINE(MCP_PROTOCOL_VERSION) = {
@@ -62,146 +63,213 @@ MCP_CLIENT *mcp_create_client(MCP_TRANSPORT transport, void *transport_ctx) {
63 mcpc->transport = transport;
64 mcpc->protocol_version = MCP_PROTOCOL_VERSION_UNKNOWN; // Will be set during initialization
65 mcpc->ready = false; // Client is not ready until initialized notification is received
65 -
66 +
67 // Set capabilities based on transport type
68 switch (transport) {
69 case MCP_TRANSPORT_WEBSOCKET:
70 mcpc->websocket = (struct websocket_server_client *)transport_ctx;
71 mcpc->capabilities = MCP_CAPABILITY_ASYNC_COMMUNICATION |
71 - MCP_CAPABILITY_SUBSCRIPTIONS |
72 + MCP_CAPABILITY_SUBSCRIPTIONS |
73 MCP_CAPABILITY_NOTIFICATIONS;
74 break;
74 -
75 +
76 case MCP_TRANSPORT_HTTP:
77 mcpc->http = (struct web_client *)transport_ctx;
78 mcpc->capabilities = MCP_CAPABILITY_NONE; // HTTP has no special capabilities
79 break;
79 -
80 +
81 + case MCP_TRANSPORT_SSE:
82 + mcpc->http = (struct web_client *)transport_ctx;
83 + mcpc->capabilities = MCP_CAPABILITY_ASYNC_COMMUNICATION |
84 + MCP_CAPABILITY_SUBSCRIPTIONS |
85 + MCP_CAPABILITY_NOTIFICATIONS;
86 + break;
87 +
88 default:
89 mcpc->generic = transport_ctx;
90 mcpc->capabilities = MCP_CAPABILITY_NONE;
91 break;
92 }
85 -
93 +
94 // Default client info (will be updated later from actual client)
95 mcpc->client_name = string_strdupz("unknown");
96 mcpc->client_version = string_strdupz("0.0.0");
89 -
97 +
98 // Set default logging level to info
99 mcpc->logging_level = MCP_LOGGING_LEVEL_INFO;
92 -
93 - // Initialize response buffers
94 - mcpc->result = buffer_create(4096, NULL);
100 +
101 + // Persistent buffers
102 mcpc->error = buffer_create(1024, NULL);
96 -
97 - // Initialize utility buffers
98 - mcpc->uri = buffer_create(1024, NULL);
99 -
100 - // Initialize request IDs tracking
101 - mcpc->request_id_counter = 0;
102 - mcpc->request_ids = NULL;
103 -
103 + mcpc->result = NULL;
104 +
105 + mcpc->last_return_code = MCP_RC_OK;
106 + mcpc->last_response_error = false;
107 +
108 return mcpc;
109 }
110
111 // Free a response context
112 void mcp_free_client(MCP_CLIENT *mcpc) {
109 - if (mcpc) {
110 - string_freez(mcpc->client_name);
111 - string_freez(mcpc->client_version);
112 -
113 - // Free response buffers
114 - buffer_free(mcpc->result);
113 + if (!mcpc)
114 + return;
115 +
116 + string_freez(mcpc->client_name);
117 + string_freez(mcpc->client_version);
118 +
119 + if (mcpc->error)
120 buffer_free(mcpc->error);
116 -
117 - // Free utility buffers
118 - buffer_free(mcpc->uri);
119 -
120 - // Free request IDs
121 - mcp_request_id_cleanup_all(mcpc);
122 -
123 - freez(mcpc);
124 - }
121 +
122 + mcp_client_release_response(mcpc);
123 +
124 + freez(mcpc);
125 }
126
127 -// Map internal MCP_RETURN_CODE to JSON-RPC error code
128 -static int mcp_map_return_code_to_jsonrpc_error(MCP_RETURN_CODE rc) {
129 - switch (rc) {
130 - case MCP_RC_OK:
131 - return 0; // Not an error
132 - case MCP_RC_INVALID_PARAMS:
133 - return -32602; // JSON-RPC Invalid params
134 - case MCP_RC_NOT_FOUND:
135 - return -32601; // JSON-RPC Method not found
136 - case MCP_RC_INTERNAL_ERROR:
137 - return -32603; // JSON-RPC Internal error
138 - case MCP_RC_NOT_IMPLEMENTED:
139 - return -32601; // Use method not found for not implemented
140 - case MCP_RC_BAD_REQUEST:
141 - return -32600; // JSON-RPC Invalid request
142 - case MCP_RC_ERROR:
143 - default:
144 - return -32000; // JSON-RPC Server error
127 +void mcp_client_clear_error(MCP_CLIENT *mcpc) {
128 + if (mcpc && mcpc->error)
129 + buffer_reset(mcpc->error);
130 +}
131 +
132 +static void mcp_client_free_chunks(MCP_CLIENT *mcpc) {
133 + if (!mcpc || !mcpc->response_chunks)
134 + return;
135 +
136 + for (size_t i = 0; i < mcpc->response_chunks_used; i++) {
137 + if (mcpc->response_chunks[i].buffer)
138 + buffer_free(mcpc->response_chunks[i].buffer);
139 }
140 +
141 + freez(mcpc->response_chunks);
142 + mcpc->response_chunks = NULL;
143 + mcpc->response_chunks_used = 0;
144 + mcpc->response_chunks_size = 0;
145 + mcpc->result = NULL;
146 }
147
148 -void mcp_init_success_result(MCP_CLIENT *mcpc, MCP_REQUEST_ID id) {
149 - buffer_flush(mcpc->result);
150 - buffer_json_initialize(mcpc->result, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY);
151 - buffer_json_member_add_string(mcpc->result, "jsonrpc", "2.0");
148 +void mcp_client_prepare_response(MCP_CLIENT *mcpc) {
149 + if (!mcpc)
150 + return;
151
153 - // Add the ID using our request ID system
154 - mcp_request_id_to_buffer(mcpc, mcpc->result, "id", id);
155 - buffer_json_member_add_object(mcpc->result, "result");
152 + mcp_client_free_chunks(mcpc);
153 + mcpc->last_return_code = MCP_RC_OK;
154 + mcpc->last_response_error = false;
155 +}
156
157 - buffer_flush(mcpc->error);
157 +void mcp_client_release_response(MCP_CLIENT *mcpc) {
158 + mcp_client_free_chunks(mcpc);
159 }
160
160 -MCP_RETURN_CODE mcp_error_result(MCP_CLIENT *mcpc, MCP_REQUEST_ID id, MCP_RETURN_CODE rc) {
161 - if (!mcpc) return rc;
162 -
163 - buffer_flush(mcpc->result);
164 - buffer_json_initialize(mcpc->result, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY);
165 - buffer_json_member_add_string(mcpc->result, "jsonrpc", "2.0");
166 -
167 - // Add the ID using our request ID system
168 - mcp_request_id_to_buffer(mcpc, mcpc->result, "id", id);
161 +static struct mcp_response_chunk *mcp_response_append_chunk(MCP_CLIENT *mcpc, enum mcp_response_chunk_type type) {
162 + if (!mcpc)
163 + return NULL;
164
170 - buffer_json_member_add_object(mcpc->result, "error");
171 - buffer_json_member_add_int64(mcpc->result, "code", mcp_map_return_code_to_jsonrpc_error(rc));
172 -
173 - const char *error_message = buffer_strlen(mcpc->error)
174 - ? buffer_tostring(mcpc->error)
175 - : MCP_RETURN_CODE_2str(rc);
176 -
177 - if(error_message && *error_message)
178 - buffer_json_member_add_string(mcpc->result, "message", error_message);
179 -
180 - buffer_json_object_close(mcpc->result); // Close error
181 -
182 - buffer_json_finalize(mcpc->result);
183 - return rc;
165 + const size_t MAX_RESPONSE_BYTES = 16 * 1024 * 1024; // 16 MiB per request safeguard
166 + if (mcp_client_response_size(mcpc) >= MAX_RESPONSE_BYTES) {
167 + netdata_log_error("MCP: response size limit reached");
168 + return NULL;
169 + }
170 +
171 + if (mcpc->response_chunks_used == mcpc->response_chunks_size) {
172 + size_t new_size = mcpc->response_chunks_size ? mcpc->response_chunks_size * 2 : 4;
173 + struct mcp_response_chunk *tmp = reallocz(mcpc->response_chunks, new_size * sizeof(*tmp));
174 + if (unlikely(!tmp))
175 + return NULL;
176 + mcpc->response_chunks = tmp;
177 + mcpc->response_chunks_size = new_size;
178 + }
179 +
180 + struct mcp_response_chunk *chunk = &mcpc->response_chunks[mcpc->response_chunks_used++];
181 + chunk->buffer = NULL;
182 + chunk->type = type;
183 + return chunk;
184 }
185
186 -// No longer needed - we're using mcp_request_id_del directly in mcp_single_request
186 +BUFFER *mcp_response_add_json_chunk(MCP_CLIENT *mcpc, size_t initial_capacity) {
187 + struct mcp_response_chunk *chunk = mcp_response_append_chunk(mcpc, MCP_RESPONSE_CHUNK_JSON);
188 + if (!chunk)
189 + return NULL;
190
188 -// Send the content of a buffer using the appropriate transport
189 -int mcp_send_response_buffer(MCP_CLIENT *mcpc) {
190 - if (!mcpc || !mcpc->result || !buffer_strlen(mcpc->result))
191 - return -1;
192 -
193 - switch (mcpc->transport) {
194 - case MCP_TRANSPORT_WEBSOCKET:
195 - return mcp_websocket_send_buffer(mcpc->websocket, mcpc->result);
196 -
197 - case MCP_TRANSPORT_HTTP:
198 - netdata_log_error("MCP: HTTP adapter not implemented yet");
199 - return -1;
191 + size_t capacity = initial_capacity ? initial_capacity : 4096;
192 + chunk->buffer = buffer_create(capacity, NULL);
193 + mcpc->result = chunk->buffer;
194 + buffer_json_initialize(chunk->buffer, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY);
195 + return chunk->buffer;
196 +}
197
201 - default:
202 - netdata_log_error("MCP: Unknown transport type %u", mcpc->transport);
203 - return -1;
198 +BUFFER *mcp_response_add_text_chunk(MCP_CLIENT *mcpc, size_t initial_capacity) {
199 + struct mcp_response_chunk *chunk = mcp_response_append_chunk(mcpc, MCP_RESPONSE_CHUNK_TEXT);
200 + if (!chunk)
201 + return NULL;
202 +
203 + size_t capacity = initial_capacity ? initial_capacity : 1024;
204 + chunk->buffer = buffer_create(capacity, NULL);
205 + chunk->buffer->content_type = CT_TEXT_PLAIN;
206 + buffer_no_cacheable(chunk->buffer);
207 + mcpc->result = chunk->buffer;
208 + return chunk->buffer;
209 +}
210 +
211 +size_t mcp_client_response_chunk_count(const MCP_CLIENT *mcpc) {
212 + return mcpc ? mcpc->response_chunks_used : 0;
213 +}
214 +
215 +const struct mcp_response_chunk *mcp_client_response_chunks(const MCP_CLIENT *mcpc) {
216 + return mcpc ? mcpc->response_chunks : NULL;
217 +}
218 +
219 +size_t mcp_client_response_size(const MCP_CLIENT *mcpc) {
220 + if (!mcpc || !mcpc->response_chunks)
221 + return 0;
222 +
223 + size_t total = 0;
224 + for (size_t i = 0; i < mcpc->response_chunks_used; i++) {
225 + if (mcpc->response_chunks[i].buffer)
226 + total += buffer_strlen(mcpc->response_chunks[i].buffer);
227 }
228 + return total;
229 +}
230 +
231 +const char *mcp_client_error_message(MCP_CLIENT *mcpc) {
232 + if (!mcpc || !mcpc->error)
233 + return NULL;
234 + return buffer_strlen(mcpc->error) ? buffer_tostring(mcpc->error) : NULL;
235 +}
236 +
237 +void mcp_init_success_result(MCP_CLIENT *mcpc, MCP_REQUEST_ID id __maybe_unused) {
238 + if (!mcpc)
239 + return;
240 +
241 + BUFFER *chunk = mcp_response_add_json_chunk(mcpc, 4096);
242 + if (!chunk)
243 + return;
244 +
245 + mcpc->last_return_code = MCP_RC_OK;
246 + mcpc->last_response_error = false;
247 + mcp_client_clear_error(mcpc);
248 +}
249 +
250 +MCP_RETURN_CODE mcp_error_result(MCP_CLIENT *mcpc, MCP_REQUEST_ID id __maybe_unused, MCP_RETURN_CODE rc) {
251 + if (!mcpc)
252 + return rc;
253 +
254 + mcpc->last_return_code = rc;
255 + mcpc->last_response_error = true;
256 +
257 + BUFFER *chunk = mcp_response_add_json_chunk(mcpc, 512);
258 + if (!chunk)
259 + return rc;
260 +
261 + const char *error_message = buffer_strlen(mcpc->error)
262 + ? buffer_tostring(mcpc->error)
263 + : MCP_RETURN_CODE_2str(rc);
264 +
265 + buffer_json_member_add_string(chunk, "status", "error");
266 + buffer_json_member_add_string(chunk, "code", MCP_RETURN_CODE_2str(rc));
267 + buffer_json_member_add_int64(chunk, "codeNumeric", rc);
268 + if (error_message)
269 + buffer_json_member_add_string(chunk, "message", error_message);
270 + buffer_json_finalize(chunk);
271 +
272 + return rc;
273 }
274
275 // Parse and extract client info from initialize request params
@@ -224,246 +292,95 @@ static void mcp_extract_client_info(MCP_CLIENT *mcpc, struct json_object *params
292 }
293 }
294
227 -// Handle a JSON-RPC method call - the result is always filled with a jsonrpc response
228 -static MCP_RETURN_CODE mcp_single_request(MCP_CLIENT *mcpc, struct json_object *request) {
229 - if (!mcpc || !request) {
230 - return MCP_RC_ERROR;
231 - }
295 +MCP_RETURN_CODE mcp_dispatch_method(MCP_CLIENT *mcpc, const char *method, struct json_object *params, MCP_REQUEST_ID id __maybe_unused) {
296 + if (!mcpc)
297 + return MCP_RC_INTERNAL_ERROR;
298
233 - // Flush buffers before processing the request
234 - buffer_reset(mcpc->result);
235 - buffer_reset(mcpc->error);
236 -
237 - // Extract JSON-RPC fields
238 - struct json_object *method_obj = NULL;
239 - struct json_object *params_obj = NULL;
240 - struct json_object *jsonrpc_obj = NULL;
241 -
242 - // Validate jsonrpc version
243 - if (!json_object_object_get_ex(request, "jsonrpc", &jsonrpc_obj) ||
244 - strcmp(json_object_get_string(jsonrpc_obj), "2.0") != 0) {
245 - buffer_strcat(mcpc->error, "Invalid or missing jsonrpc version");
299 + if (!method || !*method) {
300 + buffer_strcat(mcpc->error, "Empty method name");
301 mcp_error_result(mcpc, 0, MCP_RC_INVALID_PARAMS);
302 return MCP_RC_INVALID_PARAMS;
303 }
249 -
250 - // Extract method
251 - if (!json_object_object_get_ex(request, "method", &method_obj)) {
252 - buffer_strcat(mcpc->error, "Missing method field");
304 +
305 + if (!params || json_object_get_type(params) != json_type_object) {
306 + buffer_strcat(mcpc->error, "Parameters must be an object");
307 mcp_error_result(mcpc, 0, MCP_RC_INVALID_PARAMS);
308 return MCP_RC_INVALID_PARAMS;
309 }
256 -
257 - const char *method = json_object_get_string(method_obj);
258 -
259 - // Extract params (optional)
260 - bool params_created = false;
261 - if (json_object_object_get_ex(request, "params", &params_obj)) {
262 - if (json_object_get_type(params_obj) != json_type_object) {
263 - buffer_strcat(mcpc->error, "params must be an object");
264 - mcp_error_result(mcpc, 0, MCP_RC_INVALID_PARAMS);
265 - return MCP_RC_INVALID_PARAMS;
266 - }
267 - } else {
268 - // Create an empty params object if none provided
269 - params_obj = json_object_new_object();
270 - params_created = true;
271 - }
272 -
273 - // Extract and register the request ID
274 - MCP_REQUEST_ID id = mcp_request_id_add(mcpc, request);
275 - bool has_id = (id != 0);
276 -
277 - // If we have a request ID, log it
278 - if (has_id) {
279 - netdata_log_debug(D_WEB_CLIENT, "MCP: Handling method call: %s (request_id: %zu)", method, id);
280 - } else {
281 - netdata_log_debug(D_WEB_CLIENT, "MCP: Handling notification: %s (no id)", method);
282 - }
283 -
284 - // Handle method calls based on namespace
285 - MCP_RETURN_CODE rc;
310
287 - // Check for notifications/initialized method which marks client as ready
288 - if(!method || !*method) {
289 - buffer_strcat(mcpc->error, "Empty method name");
290 - rc = MCP_RC_INVALID_PARAMS;
291 - }
292 - else if (strcmp(method, "notifications/initialized") == 0) {
311 + MCP_RETURN_CODE rc = MCP_RC_OK;
312 +
313 + if (strcmp(method, "notifications/initialized") == 0) {
314 mcpc->ready = true;
294 - netdata_log_debug(D_WEB_CLIENT, "MCP client %s v%s is now ready",
295 - string2str(mcpc->client_name), string2str(mcpc->client_version));
296 - rc = MCP_RC_OK;
315 + netdata_log_debug(D_WEB_CLIENT, "MCP client %s v%s is now ready",
316 + string2str(mcpc->client_name), string2str(mcpc->client_version));
317 + mcp_client_prepare_response(mcpc);
318 + mcp_init_success_result(mcpc, 0);
319 + buffer_json_finalize(mcpc->result);
320 + return MCP_RC_OK;
321 + }
322 +
323 + if (!mcpc->ready && strcmp(method, "initialize") != 0) {
324 + netdata_log_debug(D_WEB_CLIENT, "MCP method %s called before initialize", method);
325 }
298 - else if (strncmp(method, "tools/", 6) == 0) {
299 - // Tools namespace
300 - rc = mcp_tools_route(mcpc, method + 6, params_obj, id);
301 - // Mark client as ready if not already
302 - if (!mcpc->ready) {
326 +
327 + mcp_client_prepare_response(mcpc);
328 + mcp_client_clear_error(mcpc);
329 +
330 + if (strncmp(method, "tools/", 6) == 0) {
331 + rc = mcp_tools_route(mcpc, method + 6, params, 0);
332 + if (!mcpc->ready)
333 mcpc->ready = true;
304 - }
334 }
335 else if (strncmp(method, "resources/", 10) == 0) {
307 - // Resources namespace
308 - rc = mcp_resources_route(mcpc, method + 10, params_obj, id);
309 - // Mark client as ready if not already
310 - if (!mcpc->ready) {
336 + rc = mcp_resources_route(mcpc, method + 10, params, 0);
337 + if (!mcpc->ready)
338 mcpc->ready = true;
312 - }
339 }
340 else if (strncmp(method, "prompts/", 8) == 0) {
315 - // Prompts namespace
316 - rc = mcp_prompts_route(mcpc, method + 8, params_obj, id);
317 - // Mark client as ready if not already
318 - if (!mcpc->ready) {
341 + rc = mcp_prompts_route(mcpc, method + 8, params, 0);
342 + if (!mcpc->ready)
343 mcpc->ready = true;
320 - }
344 }
345 else if (strncmp(method, "logging/", 8) == 0) {
323 - // Logging namespace - don't alter ready state
324 - rc = mcp_logging_route(mcpc, method + 8, params_obj, id);
346 + rc = mcp_logging_route(mcpc, method + 8, params, 0);
347 }
348 else if (strncmp(method, "completion/", 11) == 0) {
327 - // Completion namespace
328 - rc = mcp_completion_route(mcpc, method + 11, params_obj, id);
329 - // Mark client as ready if not already
330 - if (!mcpc->ready) {
349 + rc = mcp_completion_route(mcpc, method + 11, params, 0);
350 + if (!mcpc->ready)
351 mcpc->ready = true;
332 - }
352 }
353 else if (strcmp(method, "initialize") == 0) {
335 - // Extract client info from initialize request
336 - mcp_extract_client_info(mcpc, params_obj);
337 - netdata_log_debug(D_WEB_CLIENT, "MCP initialize request from client %s v%s",
354 + mcp_extract_client_info(mcpc, params);
355 + netdata_log_debug(D_WEB_CLIENT, "MCP initialize request from client %s v%s",
356 string2str(mcpc->client_name), string2str(mcpc->client_version));
339 -
340 - // Handle initialize method
341 - rc = mcp_method_initialize(mcpc, params_obj, id);
357 + rc = mcp_method_initialize(mcpc, params, 0);
358 }
359 else if (strcmp(method, "ping") == 0) {
344 - // Handle ping method - simple connection health check
345 - // Don't alter ready state for ping requests
346 - rc = mcp_method_ping(mcpc, params_obj, id);
360 + rc = mcp_method_ping(mcpc, params, 0);
361 }
362 else {
363 buffer_sprintf(mcpc->error, "Method '%s' not found", method);
364 rc = MCP_RC_NOT_FOUND;
351 - // Method not found shouldn't alter ready state
365 }
366
354 - // If this is a notification (no ID), don't generate a response
355 - if (!has_id) {
356 - // Clean up the params object if we created it
357 - if (params_created) {
358 - json_object_put(params_obj);
359 - }
360 - return rc;
361 - }
367 + if (rc != MCP_RC_OK)
368 + mcp_error_result(mcpc, 0, rc);
369
363 - // For requests with IDs, ensure we have a valid response
364 - if (rc != MCP_RC_OK && !buffer_strlen(mcpc->result)) {
365 - mcp_error_result(mcpc, id, rc);
366 - }
367 -
368 - if (!buffer_strlen(mcpc->result)) {
370 + // Ensure at least one chunk exists on success
371 + if (rc == MCP_RC_OK && mcp_client_response_chunk_count(mcpc) == 0) {
372 buffer_strcat(mcpc->error, "method generated empty result");
370 - mcp_error_result(mcpc, id, MCP_RC_INTERNAL_ERROR);
371 - }
372 -
373 - // Clean up the request ID
374 - mcp_request_id_del(mcpc, id);
375 -
376 - // Clean up the params object if we created it
377 - if (params_created) {
378 - json_object_put(params_obj);
373 + rc = mcp_error_result(mcpc, 0, MCP_RC_INTERNAL_ERROR);
374 }
375
376 return rc;
377 }
378
384 -// Main MCP entry point - handle a JSON-RPC request (can be single or batch)
385 -MCP_RETURN_CODE mcp_handle_request(MCP_CLIENT *mcpc, struct json_object *request) {
386 - if (!mcpc || !request)
387 - return MCP_RC_INTERNAL_ERROR;
388 -
389 - // Clear previous response buffers
390 - buffer_flush(mcpc->result);
391 - buffer_flush(mcpc->error);
392 -
393 - // Check if this is a batch request (JSON array)
394 - if (json_object_get_type(request) == json_type_array) {
395 - int array_len = (int)json_object_array_length(request);
396 -
397 - // Empty batch should return nothing according to JSON-RPC 2.0 spec
398 - if (array_len == 0) {
399 - return MCP_RC_OK;
400 - }
401 -
402 - // Create a temporary buffer for building the batch response
403 - BUFFER *batch_buffer = buffer_create(4096, NULL);
404 - buffer_flush(batch_buffer);
405 -
406 - // Start the JSON array for batch response
407 - buffer_strcat(batch_buffer, "[");
408 -
409 - // Track if we've added any responses (for comma handling)
410 - size_t responses_added = 0;
411 -
412 - // Process each request in the batch
413 - for (int i = 0; i < array_len; i++) {
414 - struct json_object *req_item = json_object_array_get_idx(request, i);
415 -
416 - // Process the individual request
417 - buffer_flush(mcpc->result);
418 - buffer_flush(mcpc->error);
419 -
420 - // Call the single request handler
421 - mcp_single_request(mcpc, req_item);
422 -
423 - // For notifications (no id), don't add to response
424 - if (buffer_strlen(mcpc->result) == 0) {
425 - continue;
426 - }
427 -
428 - // Add comma if this isn't the first response
429 - if (responses_added) {
430 - buffer_strcat(batch_buffer, ", ");
431 - }
432 -
433 - // Add the response to the batch
434 - buffer_strcat(batch_buffer, buffer_tostring(mcpc->result));
435 - responses_added++;
436 - }
437 -
438 - // If no responses were added (all notifications), don't send anything per JSON-RPC spec
439 - if (!responses_added) {
440 - buffer_free(batch_buffer);
441 - return MCP_RC_OK;
442 - }
443 -
444 - // Close the JSON array
445 - buffer_strcat(batch_buffer, "]");
446 -
447 - // Copy batch response to client's result buffer
448 - buffer_flush(mcpc->result);
449 - buffer_strcat(mcpc->result, buffer_tostring(batch_buffer));
450 - buffer_free(batch_buffer);
451 -
452 - // Send the batch response
453 - mcp_send_response_buffer(mcpc);
454 -
455 - return MCP_RC_OK;
456 - }
457 - else {
458 - // Handle single request
459 - MCP_RETURN_CODE rc = mcp_single_request(mcpc, request);
460 - mcp_send_response_buffer(mcpc);
461 - return rc;
462 - }
463 -}
464 -
379 // Initialize the MCP subsystem
380 void mcp_initialize_subsystem(void) {
381 + if (unlikely(mcp_initialized))
382 + return;
383 +
384 mcp_functions_registry_init();
385
386 #ifdef NETDATA_MCP_DEV_PREVIEW_API_KEY
@@ -473,4 +390,5 @@ void mcp_initialize_subsystem(void) {
390 // debug_flags |= D_MCP;
391
392 netdata_log_info("MCP subsystem initialized");
393 + mcp_initialized = true;
394 }
src/web/mcp/mcp.h
+35 -18
@@ -5,7 +5,10 @@
5
6 #include "libnetdata/libnetdata.h"
7 #include <json-c/json.h>
8 -#include "mcp-request-id.h"
8 +#include "libnetdata/buffer/buffer.h"
9 +
10 +// Request ID type - adapters may use 0 when no correlation is required
11 +typedef size_t MCP_REQUEST_ID;
12
13 // MCP tool names - use these constants when referring to tools
14 #define MCP_TOOL_LIST_METRICS "list_metrics"
@@ -126,6 +129,7 @@ typedef enum {
129 MCP_TRANSPORT_UNKNOWN = 0,
130 MCP_TRANSPORT_WEBSOCKET,
131 MCP_TRANSPORT_HTTP,
132 + MCP_TRANSPORT_SSE,
133 // Add more as needed
134 } MCP_TRANSPORT;
135
@@ -179,17 +183,24 @@ typedef struct mcp_client {
183
184 // Logging configuration
185 MCP_LOGGING_LEVEL logging_level; // Current logging level set by client
182 -
183 - // Response buffers
184 - BUFFER *result; // Pre-allocated buffer for success responses
185 - BUFFER *error; // Pre-allocated buffer for error messages
186 -
187 - // Utility buffers
188 - BUFFER *uri; // Pre-allocated buffer for URI decoding
189 -
190 - // Request IDs tracking
191 - size_t request_id_counter; // Counter for generating sequential request IDs
192 - Pvoid_t request_ids; // JudyL array for mapping internal IDs to client IDs
186 +
187 + // Per-request response data
188 + BUFFER *error; // Persistent buffer accumulating error messages
189 + BUFFER *result; // Convenience pointer to currently active response chunk
190 + struct mcp_response_chunk {
191 + BUFFER *buffer; // Response payload
192 + enum mcp_response_chunk_type {
193 + MCP_RESPONSE_CHUNK_JSON = 0,
194 + MCP_RESPONSE_CHUNK_TEXT,
195 + MCP_RESPONSE_CHUNK_BINARY,
196 + } type; // Encoding hint for adapters
197 + } *response_chunks;
198 + size_t response_chunks_used;
199 + size_t response_chunks_size;
200 +
201 + // Last response status
202 + MCP_RETURN_CODE last_return_code;
203 + bool last_response_error;
204 } MCP_CLIENT;
205
206 // Helper function to convert string version to numeric version
@@ -206,12 +217,18 @@ void mcp_free_client(MCP_CLIENT *mcpc);
217
218 // Helper functions for creating and sending JSON-RPC responses
219
209 -// Functions to initialize and build MCP responses
220 +// Response lifecycle helpers
221 +void mcp_client_prepare_response(MCP_CLIENT *mcpc);
222 +void mcp_client_release_response(MCP_CLIENT *mcpc);
223 +BUFFER *mcp_response_add_json_chunk(MCP_CLIENT *mcpc, size_t initial_capacity);
224 +BUFFER *mcp_response_add_text_chunk(MCP_CLIENT *mcpc, size_t initial_capacity);
225 +size_t mcp_client_response_chunk_count(const MCP_CLIENT *mcpc);
226 +const struct mcp_response_chunk *mcp_client_response_chunks(const MCP_CLIENT *mcpc);
227 +size_t mcp_client_response_size(const MCP_CLIENT *mcpc);
228 void mcp_init_success_result(MCP_CLIENT *mcpc, MCP_REQUEST_ID id);
229 MCP_RETURN_CODE mcp_error_result(MCP_CLIENT *mcpc, MCP_REQUEST_ID id, MCP_RETURN_CODE rc);
212 -
213 -// Send prepared buffer content as response
214 -int mcp_send_response_buffer(MCP_CLIENT *mcpc);
230 +const char *mcp_client_error_message(MCP_CLIENT *mcpc);
231 +void mcp_client_clear_error(MCP_CLIENT *mcpc);
232
233 // Check if a capability is supported by the transport
234 static inline bool mcp_has_capability(MCP_CLIENT *mcpc, MCP_CAPABILITY capability) {
@@ -221,7 +238,7 @@ static inline bool mcp_has_capability(MCP_CLIENT *mcpc, MCP_CAPABILITY capabilit
238 // Initialize the MCP subsystem
239 void mcp_initialize_subsystem(void);
240
224 -// Main MCP entry point - handle a JSON-RPC request (single or batch)
225 -MCP_RETURN_CODE mcp_handle_request(MCP_CLIENT *mcpc, struct json_object *request);
241 +// Transport-agnostic dispatcher (method string follows MCP namespace semantics)
242 +MCP_RETURN_CODE mcp_dispatch_method(MCP_CLIENT *mcpc, const char *method, struct json_object *params, MCP_REQUEST_ID id);
243
244 #endif // NETDATA_MCP_H
src/web/server/web_client.c
+21 -1
@@ -2,6 +2,8 @@
2
3 #include "web_client.h"
4 #include "web/websocket/websocket.h"
5 +#include "web/mcp/adapters/mcp-http.h"
6 +#include "web/mcp/adapters/mcp-sse.h"
7
8 // this is an async I/O implementation of the web server request parser
9 // it is used by all netdata web servers
@@ -35,6 +37,7 @@ void web_client_reset_permissions(struct web_client *w) {
37 w->user_auth.method = USER_AUTH_METHOD_NONE;
38 w->user_auth.access = HTTP_ACCESS_NONE;
39 w->user_auth.user_role = HTTP_USER_ROLE_NONE;
40 + web_client_clear_mcp_preview_key(w);
41 }
42
43 void web_client_set_permissions(struct web_client *w, HTTP_ACCESS access, HTTP_USER_ROLE role, USER_AUTH_METHOD type) {
@@ -190,6 +193,9 @@ static void web_client_reset_allocations(struct web_client *w, bool free_all) {
193
194 web_client_reset_permissions(w);
195 web_client_flag_clear(w, WEB_CLIENT_ENCODING_GZIP|WEB_CLIENT_ENCODING_DEFLATE);
196 + web_client_flag_clear(w, WEB_CLIENT_FLAG_ACCEPT_JSON |
197 + WEB_CLIENT_FLAG_ACCEPT_SSE |
198 + WEB_CLIENT_FLAG_ACCEPT_TEXT);
199 web_client_reset_path_flags(w);
200 }
201
@@ -1115,7 +1121,9 @@ static inline int web_client_process_url(RRDHOST *host, struct web_client *w, ch
1121 hash_v0 = 0,
1122 hash_v1 = 0,
1123 hash_v2 = 0,
1118 - hash_v3 = 0;
1124 + hash_v3 = 0,
1125 + hash_mcp = 0,
1126 + hash_sse = 0;
1127
1128 #ifdef NETDATA_INTERNAL_CHECKS
1129 static uint32_t hash_exit = 0, hash_debug = 0, hash_mirror = 0;
@@ -1130,6 +1138,8 @@ static inline int web_client_process_url(RRDHOST *host, struct web_client *w, ch
1138 hash_v1 = simple_hash("v1");
1139 hash_v2 = simple_hash("v2");
1140 hash_v3 = simple_hash("v3");
1141 + hash_mcp = simple_hash("mcp");
1142 + hash_sse = simple_hash("sse");
1143 #ifdef NETDATA_INTERNAL_CHECKS
1144 hash_exit = simple_hash("exit");
1145 hash_debug = simple_hash("debug");
@@ -1150,6 +1160,16 @@ static inline int web_client_process_url(RRDHOST *host, struct web_client *w, ch
1160 netdata_log_debug(D_WEB_CLIENT_ACCESS, "%llu: API request ...", w->id);
1161 return check_host_and_call(host, w, decoded_url_path, web_client_api_request);
1162 }
1163 + else if(likely(hash == hash_mcp && strcmp(tok, "mcp") == 0)) {
1164 + if(unlikely(!http_can_access_dashboard(w)))
1165 + return web_client_permission_denied_acl(w);
1166 + return mcp_http_handle_request(host, w);
1167 + }
1168 + else if(likely(hash == hash_sse && strcmp(tok, "sse") == 0)) {
1169 + if(unlikely(!http_can_access_dashboard(w)))
1170 + return web_client_permission_denied_acl(w);
1171 + return mcp_sse_handle_request(host, w);
1172 + }
1173 else if(unlikely((hash == hash_host && strcmp(tok, "host") == 0) || (hash == hash_node && strcmp(tok, "node") == 0))) { // host switching
1174 netdata_log_debug(D_WEB_CLIENT_ACCESS, "%llu: host switch request ...", w->id);
1175 return web_client_switch_host(host, w, decoded_url_path, hash == hash_node, web_client_process_url);
src/web/server/web_client.h
+8
@@ -66,6 +66,10 @@ typedef enum __attribute__((packed)) {
66 // websocket flags
67 WEB_CLIENT_FLAG_WEBSOCKET_CLIENT = (1 << 23), // this is a websocket client
68 WEB_CLIENT_FLAG_WEBSOCKET_HANDSHAKE = (1 << 24), // websocket handshake detected
69 + WEB_CLIENT_FLAG_ACCEPT_JSON = (1 << 25),
70 + WEB_CLIENT_FLAG_ACCEPT_SSE = (1 << 26),
71 + WEB_CLIENT_FLAG_ACCEPT_TEXT = (1 << 27),
72 + WEB_CLIENT_FLAG_MCP_PREVIEW_KEY = (1 << 28), // Authorization header matched MCP preview key
73 } WEB_CLIENT_FLAGS;
74
75 #define WEB_CLIENT_FLAG_PATH_WITH_VERSION (WEB_CLIENT_FLAG_PATH_IS_V0|WEB_CLIENT_FLAG_PATH_IS_V1|WEB_CLIENT_FLAG_PATH_IS_V2|WEB_CLIENT_FLAG_PATH_IS_V3)
@@ -106,6 +110,10 @@ typedef enum __attribute__((packed)) {
110 #define web_client_enable_ssl_wait_send(w) web_client_flag_set(w, WEB_CLIENT_FLAG_SSL_WAIT_SEND)
111 #define web_client_disable_ssl_wait_send(w) web_client_flag_clear(w, WEB_CLIENT_FLAG_SSL_WAIT_SEND)
112
113 +#define web_client_has_mcp_preview_key(w) web_client_flag_check(w, WEB_CLIENT_FLAG_MCP_PREVIEW_KEY)
114 +#define web_client_set_mcp_preview_key(w) web_client_flag_set(w, WEB_CLIENT_FLAG_MCP_PREVIEW_KEY)
115 +#define web_client_clear_mcp_preview_key(w) web_client_flag_clear(w, WEB_CLIENT_FLAG_MCP_PREVIEW_KEY)
116 +
117 #define web_client_check_conn_unix(w) web_client_flag_check(w, WEB_CLIENT_FLAG_CONN_UNIX)
118 #define web_client_check_conn_tcp(w) web_client_flag_check(w, WEB_CLIENT_FLAG_CONN_TCP)
119 #define web_client_check_conn_cloud(w) web_client_flag_check(w, WEB_CLIENT_FLAG_CONN_CLOUD)
src/web/websocket/websocket-handshake.c
+31 -24
@@ -5,7 +5,7 @@
5 #include "websocket-jsonrpc.h"
6 #include "websocket-echo.h"
7 #include "../mcp/adapters/mcp-websocket.h"
8 -#include "../mcp/mcp-api-key.h"
8 +#include "web/api/mcp_auth.h"
9
10 // Global array of WebSocket threads
11 WEBSOCKET_THREAD websocket_threads[WEBSOCKET_MAX_THREADS];
@@ -332,29 +332,36 @@ short int websocket_handle_handshake(struct web_client *w) {
332 }
333
334 #ifdef NETDATA_MCP_DEV_PREVIEW_API_KEY
335 - // Check for api_key parameter for MCP developer preview
336 - char *api_key_str = strstr(query, "api_key=");
337 - if (api_key_str) {
338 - api_key_str += strlen("api_key=");
339 -
340 - // Extract the API key value (until & or end of string)
341 - char api_key_buffer[MCP_DEV_PREVIEW_API_KEY_LENGTH + 1];
342 - size_t i = 0;
343 - while (api_key_str[i] && api_key_str[i] != '&' && i < MCP_DEV_PREVIEW_API_KEY_LENGTH) {
344 - api_key_buffer[i] = api_key_str[i];
345 - i++;
346 - }
347 - api_key_buffer[i] = '\0';
348 -
349 - // Verify the API key
350 - if (mcp_api_key_verify(api_key_buffer)) {
351 - // Override authentication with god mode
352 - wsc->user_auth.access = HTTP_ACCESS_ALL;
353 - wsc->user_auth.method = USER_AUTH_METHOD_GOD;
354 - wsc->user_auth.user_role = HTTP_USER_ROLE_ADMIN;
355 - websocket_debug(wsc, "MCP developer preview API key verified - enabling full access");
356 - } else {
357 - websocket_debug(wsc, "Invalid MCP developer preview API key provided");
335 + if (web_client_has_mcp_preview_key(w)) {
336 + wsc->user_auth.access = HTTP_ACCESS_ALL;
337 + wsc->user_auth.method = USER_AUTH_METHOD_GOD;
338 + wsc->user_auth.user_role = HTTP_USER_ROLE_ADMIN;
339 + websocket_debug(wsc, "MCP developer preview API key verified via Authorization header - enabling full access");
340 + } else {
341 + // Check for api_key parameter for MCP developer preview
342 + char *api_key_str = strstr(query, "api_key=");
343 + if (api_key_str) {
344 + api_key_str += strlen("api_key=");
345 +
346 + // Extract the API key value (until & or end of string)
347 + char api_key_buffer[MCP_DEV_PREVIEW_API_KEY_LENGTH + 1];
348 + size_t i = 0;
349 + while (api_key_str[i] && api_key_str[i] != '&' && i < MCP_DEV_PREVIEW_API_KEY_LENGTH) {
350 + api_key_buffer[i] = api_key_str[i];
351 + i++;
352 + }
353 + api_key_buffer[i] = '\0';
354 +
355 + // Verify the API key
356 + if (mcp_api_key_verify(api_key_buffer)) {
357 + // Override authentication with god mode
358 + wsc->user_auth.access = HTTP_ACCESS_ALL;
359 + wsc->user_auth.method = USER_AUTH_METHOD_GOD;
360 + wsc->user_auth.user_role = HTTP_USER_ROLE_ADMIN;
361 + websocket_debug(wsc, "MCP developer preview API key verified - enabling full access");
362 + } else {
363 + websocket_debug(wsc, "Invalid MCP developer preview API key provided");
364 + }
365 }
366 }
367 #endif