Skip to content
This repository was archived by the owner on Feb 28, 2026. It is now read-only.

Commit 96eca43

Browse files
authored
Merge pull request #41 from NuclearPlayer/feat/mcp
feat: MCP server for AI agent control
2 parents 5ddcd08 + 8d22e7d commit 96eca43

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2571
-101
lines changed

packages/docs/SUMMARY.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
* [Dashboard](plugins/dashboard.md)
1313
* [Providers](plugins/providers.md)
1414

15+
## Integrations
16+
17+
* [MCP Server](integrations/mcp-server.md)
18+
1519
## Theming
1620

1721
* [Themes](themes/themes.md)
@@ -21,6 +25,7 @@
2125
## Development
2226

2327
* [Logging](development/logging.md)
28+
* [MCP Architecture](development/mcp-architecture.md)
2429

2530
## Misc
2631

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
---
2+
description: How the MCP server works internally and how to extend it.
3+
---
4+
5+
# MCP architecture
6+
7+
Nuclear's [MCP](https://modelcontextprotocol.io/) server lets your AI control the music player and do anything that plugins can do!
8+
9+
The server runs on `localhost:8800/mcp` using the Streamable HTTP transport.
10+
11+
## The four MCP tools
12+
13+
Rust defines four tools in `tools.rs`. No business logic lives in Rust. These tools reuse the same API as plugins.
14+
15+
| Tool | Purpose | Arguments |
16+
|------|---------|-----------|
17+
| `list_methods` | List methods in a domain | `{ domain: "Queue" }` |
18+
| `method_details` | Get parameter names, types, return type for a method | `{ method: "Queue.addToQueue" }` |
19+
| `describe_type` | Get the JSON shape of a data type (Track, QueueItem, etc.) | `{ type: "Track" }` |
20+
| `call` | Execute a method | `{ method: "Queue.addToQueue", params: { tracks: [...] } }` |
21+
22+
The first three are discovery tools. Agents use them to figure out what's available before calling anything. The TS handler serves these from static metadata objects (`apiMeta` and `typeRegistry` from `@nuclearplayer/plugin-sdk/mcp`).
23+
24+
The `call` tool runs through the dispatcher, which converts named parameters to positional arguments and calls the API method.
25+
26+
## The Rust/JS bridge protocol
27+
28+
Rust and JS communicate through Tauri's event system with a request/response pattern correlated by trace IDs.
29+
30+
### Request flow
31+
32+
1. An agent calls a tool. Rust receives the HTTP request.
33+
2. Rust generates a UUID trace ID, stores a `oneshot::Sender` in a `HashMap<String, Sender>`, and emits an `mcp:tool-call` event to the webview with `{ traceId, toolName, arguments }`.
34+
3. JS receives the event, validates the payload with Zod, and routes to the appropriate handler.
35+
4. For `list_methods`, `method_details`, and `describe_type`: JS looks up the answer in `apiMeta` or `typeRegistry`. No API call happens.
36+
5. For `call`: the dispatcher parses `"Queue.addToQueue"` into domain + method, looks up the `MethodMeta` to get the parameter order, converts the named params object into positional args, and calls the method on `NuclearPluginAPI`.
37+
6. JS calls `invoke('mcp_respond', { response: { traceId, success, data } })` (or `{ traceId, success: false, error }` on failure).
38+
7. Rust receives the `mcp_respond` command, looks up the trace ID in the pending map, and sends the response through the oneshot channel. The original `call_tool` function was awaiting this channel and now returns the result to the HTTP response.
39+
40+
### Timeout and error handling
41+
42+
The bridge times out after 30 seconds. If JS doesn't respond in that window, Rust removes the pending entry and returns an error.
43+
44+
There are two error types:
45+
46+
- `InfrastructureError`: The bridge itself broke. Timeout, channel closed, event emission failed. Rust logs the error and returns it as an MCP protocol error (internal error). This means something is wrong with the bridge, not with the requested operation.
47+
- `ToolError`: The method ran but returned an error (e.g., "unknown domain", "playlist not found"). Rust passes this through as tool error content, which the agent can read and react to.
48+
49+
## Server lifecycle
50+
51+
`mcp_start` and `mcp_stop` are Tauri commands (not events), so the JS caller gets a `Result` back and can handle errors.
52+
53+
### Startup
54+
55+
MCP tries to bind to `localhost:8800` by default, but if that port is taken, it tries the next one up, up to 8809. If all are taken, it returns an error.
56+
57+
### Settings
58+
59+
The server can be disabled or enabled in the settings, and shows you its URL.
60+
61+
### Metadata types
62+
63+
Defined in `meta.ts`:
64+
65+
```typescript
66+
type ParamMeta = {
67+
name: string;
68+
type: string;
69+
};
70+
71+
type MethodMeta = {
72+
name: string;
73+
description: string;
74+
params: ParamMeta[];
75+
returns: string;
76+
};
77+
78+
type DomainMeta = {
79+
description: string;
80+
methods: Record<string, MethodMeta>;
81+
};
82+
```
83+
84+
Each domain has a `*.meta.ts` file (e.g., `queue.meta.ts`) that exports a `DomainMeta` object. The `apiMeta` object in `meta.ts` aggregates all domain metadata by importing these files.
85+
86+
The `typeRegistry` in `typeRegistry.ts` maps type names (like `"Track"`, `"QueueItem"`) to their field definitions so agents can call `describe_type` and understand the data shapes.
87+
88+
## How to add a new domain
89+
90+
1. Create the Host interface, host implementation, and API class following the standard plugin SDK pattern (see [adding new domains](../plugin-sdk/adding-domains.md)).
91+
2. Create a `yourdomain.meta.ts` file in `packages/plugin-sdk/src/mcp/` exporting a `DomainMeta`.
92+
3. Import and register it in the `apiMeta` object in `packages/plugin-sdk/src/mcp/meta.ts`.
93+
4. Update the domain list in the `list_methods` tool description string in `packages/player/src-tauri/src/mcp/tools.rs`. This is the only Rust change needed when adding a domain.
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
---
2+
description: Let AI agents control Nuclear via the Model Context Protocol.
3+
---
4+
5+
# MCP server
6+
7+
Nuclear includes a built-in [MCP](https://modelcontextprotocol.io/) server that lets your AI control the music player, doing pretty much anything that you can!
8+
9+
## Enable the server
10+
11+
1. Open Nuclear → Settings → Integrations.
12+
2. Toggle `Enable MCP Server` on.
13+
3. The server starts on `http://127.0.0.1:8800/mcp` (localhost only). If port 8800 is taken, it tries 8801, 8802, and so on up to 8809.
14+
4. The **MCP Server URL** field below the toggle shows the actual URL. Click the copy button to grab it.
15+
16+
## Connect your AI tool
17+
18+
The server URL is `http://127.0.0.1:8800/mcp` using the `Streamable HTTP` transport.
19+
20+
{% tabs %}
21+
{% tab title="Claude Code" %}
22+
```bash
23+
claude mcp add nuclear --transport http http://127.0.0.1:8800/mcp
24+
```
25+
{% endtab %}
26+
27+
{% tab title="OpenCode" %}
28+
Add to your `opencode.json`:
29+
30+
```json
31+
{
32+
"mcp": {
33+
"nuclear": {
34+
"type": "remote",
35+
"url": "http://127.0.0.1:8800/mcp"
36+
}
37+
}
38+
}
39+
```
40+
{% endtab %}
41+
42+
{% tab title="Codex CLI" %}
43+
Add to `~/.codex/config.toml`:
44+
45+
```toml
46+
[mcp_servers.nuclear]
47+
url = "http://127.0.0.1:8800/mcp"
48+
```
49+
50+
Or via the CLI:
51+
52+
```bash
53+
codex mcp add nuclear --url http://127.0.0.1:8800/mcp
54+
```
55+
{% endtab %}
56+
57+
{% tab title="Claude Desktop / Cursor / Windsurf" %}
58+
Add to your MCP config (`claude_desktop_config.json`, `.cursor/mcp.json`, etc.):
59+
60+
```json
61+
{
62+
"mcpServers": {
63+
"nuclear": {
64+
"url": "http://127.0.0.1:8800/mcp"
65+
}
66+
}
67+
}
68+
```
69+
{% endtab %}
70+
71+
{% tab title="MCP Inspector" %}
72+
```bash
73+
npx @modelcontextprotocol/inspector
74+
```
75+
76+
Enter `http://127.0.0.1:8800/mcp` as the URL and select `Streamable HTTP` as the transport.
77+
{% endtab %}
78+
{% endtabs %}
79+
80+
## Tools
81+
82+
Nuclear exposes four MCP tools. The server uses a hierarchical discovery pattern: start broad, drill down, then act.
83+
84+
### `list_methods`
85+
86+
Lists available methods in a domain.
87+
88+
| Parameter | Type | Required | Description |
89+
| --------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------- |
90+
| `domain` | string | yes | One of: `Queue`, `Playback`, `Metadata`, `Favorites`, `Playlists`, `Dashboard`, `Providers`. |
91+
92+
Returns the method names and short descriptions for that domain.
93+
94+
### `method_details`
95+
96+
Gets full details for a single method: its description, parameter names and types, and return type.
97+
98+
| Parameter | Type | Required | Description |
99+
| --------- | ------ | -------- | ----------------------------------------------------------------- |
100+
| `method` | string | yes | The method name in `Domain.method` format, e.g. `Queue.addToQueue`. |
101+
102+
### `describe_type`
103+
104+
Gets the JSON shape of a data type. Use this when `method_details` returns a parameter or return type that references a complex type.
105+
106+
| Parameter | Type | Required | Description |
107+
| --------- | ------ | -------- | ---------------------------------------------------- |
108+
| `type` | string | yes | The type name, e.g. `Track`, `QueueItem`, `Playlist`. |
109+
110+
### `call`
111+
112+
Calls a Nuclear API method.
113+
114+
| Parameter | Type | Required | Description |
115+
| --------- | ------ | -------- | ---------------------------------------------------------------------------------------------- |
116+
| `method` | string | yes | The method name in `Domain.method` format, e.g. `Queue.addToQueue`. |
117+
| `params` | object | no | A JSON object with named fields matching the method's parameters. Omit or pass `{}` for methods with no parameters. |
118+
119+
## Discovery workflow
120+
121+
An agent follows this sequence to find and call an API method:
122+
123+
1. Read the `list_methods` tool description to see the seven available domains.
124+
2. Call `list_methods` with a domain (e.g. `Queue`) to see that domain's methods.
125+
3. Call `method_details` (e.g. `Queue.addToQueue`) to get parameter names, types, and the return type.
126+
4. If a parameter or return type is a complex type like `Track`, call `describe_type` to see its fields.
127+
5. Call `call` with the method name and parameters to execute it.
128+
129+
Each step returns a small, focused payload to save on tokens.
130+
131+
## Agent skill
132+
133+
If your AI tool supports skills (like Claude Code), you can install one that teaches the agent how to use Nuclear's MCP tools, including the discovery workflow, common recipes, and the full API reference.
134+
135+
[Download nuclear-mcp.zip](/skills/nuclear-mcp.zip)
136+
137+
Unzip it into your skills directory (e.g. `~/.claude/skills/`) and the agent will pick it up automatically.
6.27 KB
Binary file not shown.

packages/i18n/src/locales/en_US.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,19 @@
253253
"title": "Auto-install updates",
254254
"description": "Download and install updates automatically when available"
255255
}
256+
},
257+
"integrations": {
258+
"title": "Integrations",
259+
"mcp": {
260+
"enabled": {
261+
"title": "Enable MCP Server",
262+
"description": "Start a local MCP server that allows AI tools to control Nuclear."
263+
},
264+
"serverUrl": {
265+
"title": "MCP Server URL",
266+
"description": "Point your AI tool to this URL to connect to Nuclear."
267+
}
268+
}
256269
}
257270
},
258271
"queue": {

0 commit comments

Comments
 (0)