@fluxpool/mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/bridge.d.ts +32 -0
- package/dist/bridge.js +103 -0
- package/dist/bridge.js.map +1 -0
- package/dist/client.d.ts +43 -0
- package/dist/client.js +138 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.js +43 -0
- package/dist/types.js.map +1 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fluxpool
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# @fluxpool/mcp-server
|
|
2
|
+
|
|
3
|
+
Generate images and videos with [Fluxpool.ai](https://fluxpool.ai) from Claude,
|
|
4
|
+
Cursor, or any MCP client — across Flux, Seedream, Seedance, Runway and more,
|
|
5
|
+
billed against a single pool of Fluxpool credits.
|
|
6
|
+
|
|
7
|
+
## Tools
|
|
8
|
+
|
|
9
|
+
| Tool | What it does |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `list_models` | The current model catalog — IDs and media type. Call first if unsure which model to use. |
|
|
12
|
+
| `create_generation` | Submit an image or video generation. Returns a generation ID immediately. |
|
|
13
|
+
| `check_generation` | Poll a generation's status; returns the URL once ready. |
|
|
14
|
+
| `get_generation_details` | Full metadata for one generation — prompt, params, credits charged. |
|
|
15
|
+
| `list_library` | Browse saved assets, filtered by media type or model. |
|
|
16
|
+
| `get_balance` | Credit balance for the API key. |
|
|
17
|
+
|
|
18
|
+
Generation is **asynchronous**: `create_generation` returns an ID, then
|
|
19
|
+
`check_generation` is polled until the status is `completed`. Output URLs are
|
|
20
|
+
presigned and **expire after one hour** — download promptly.
|
|
21
|
+
|
|
22
|
+
## Get an API key
|
|
23
|
+
|
|
24
|
+
Create one at **<https://app.fluxpool.ai/api>**. Keys look like `fp_live_…`.
|
|
25
|
+
|
|
26
|
+
Treat it like a password: it can spend your credits. Put it in your MCP client
|
|
27
|
+
config (below), never in a file you commit.
|
|
28
|
+
|
|
29
|
+
## Setup
|
|
30
|
+
|
|
31
|
+
Two ways to connect, both serving the same six tools.
|
|
32
|
+
|
|
33
|
+
### Option 1 — Hosted endpoint (recommended)
|
|
34
|
+
|
|
35
|
+
If your client supports remote MCP servers over HTTP, connect directly. No
|
|
36
|
+
install, no local process, nothing to keep updated.
|
|
37
|
+
|
|
38
|
+
**Endpoint:** `https://api.fluxpool.ai/v1/mcp`
|
|
39
|
+
**Auth:** `Authorization: Bearer fp_live_…`
|
|
40
|
+
|
|
41
|
+
<details>
|
|
42
|
+
<summary>Claude Code</summary>
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
claude mcp add --transport http fluxpool https://api.fluxpool.ai/v1/mcp \
|
|
46
|
+
--header "Authorization: Bearer fp_live_your_key_here"
|
|
47
|
+
```
|
|
48
|
+
</details>
|
|
49
|
+
|
|
50
|
+
<details>
|
|
51
|
+
<summary>Claude Desktop / Claude web — custom connector</summary>
|
|
52
|
+
|
|
53
|
+
Settings → Connectors → **Add custom connector**, then supply the URL
|
|
54
|
+
`https://api.fluxpool.ai/v1/mcp` and the `Authorization` header above.
|
|
55
|
+
</details>
|
|
56
|
+
|
|
57
|
+
<details>
|
|
58
|
+
<summary>Cursor — <code>.cursor/mcp.json</code></summary>
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"mcpServers": {
|
|
63
|
+
"fluxpool": {
|
|
64
|
+
"url": "https://api.fluxpool.ai/v1/mcp",
|
|
65
|
+
"headers": {
|
|
66
|
+
"Authorization": "Bearer fp_live_your_key_here"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
</details>
|
|
73
|
+
|
|
74
|
+
### Option 2 — Local bridge via npx
|
|
75
|
+
|
|
76
|
+
For clients that only speak **stdio**. This package is a thin bridge: it
|
|
77
|
+
forwards JSON-RPC to the same hosted endpoint, so the tools are identical.
|
|
78
|
+
|
|
79
|
+
No install step — `npx` fetches it on first run.
|
|
80
|
+
|
|
81
|
+
<details open>
|
|
82
|
+
<summary>Claude Desktop — <code>claude_desktop_config.json</code></summary>
|
|
83
|
+
|
|
84
|
+
- **macOS** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
85
|
+
- **Windows** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
86
|
+
- **Linux** `~/.config/Claude/claude_desktop_config.json`
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"mcpServers": {
|
|
91
|
+
"fluxpool": {
|
|
92
|
+
"command": "npx",
|
|
93
|
+
"args": ["-y", "@fluxpool/mcp-server"],
|
|
94
|
+
"env": {
|
|
95
|
+
"FLUXPOOL_API_KEY": "fp_live_your_key_here"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Restart Claude Desktop fully — quit and reopen, not just close the window.
|
|
103
|
+
</details>
|
|
104
|
+
|
|
105
|
+
<details>
|
|
106
|
+
<summary>Cursor — <code>.cursor/mcp.json</code></summary>
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"mcpServers": {
|
|
111
|
+
"fluxpool": {
|
|
112
|
+
"command": "npx",
|
|
113
|
+
"args": ["-y", "@fluxpool/mcp-server"],
|
|
114
|
+
"env": {
|
|
115
|
+
"FLUXPOOL_API_KEY": "fp_live_your_key_here"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
</details>
|
|
122
|
+
|
|
123
|
+
<details>
|
|
124
|
+
<summary>Claude Code</summary>
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
claude mcp add fluxpool \
|
|
128
|
+
--env FLUXPOOL_API_KEY=fp_live_your_key_here \
|
|
129
|
+
-- npx -y @fluxpool/mcp-server
|
|
130
|
+
```
|
|
131
|
+
</details>
|
|
132
|
+
|
|
133
|
+
Prefer a pinned global install over `npx`:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npm install -g @fluxpool/mcp-server
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Then use `"command": "fluxpool-mcp"` with no `args`.
|
|
140
|
+
|
|
141
|
+
## Verify it works
|
|
142
|
+
|
|
143
|
+
Ask your client: **"List my Fluxpool models."** It should call `list_models`
|
|
144
|
+
and print the catalog.
|
|
145
|
+
|
|
146
|
+
To check the transport by hand:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
|
|
150
|
+
| FLUXPOOL_API_KEY=fp_live_your_key_here npx -y @fluxpool/mcp-server
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
That prints one JSON line listing all six tools.
|
|
154
|
+
|
|
155
|
+
## Configuration
|
|
156
|
+
|
|
157
|
+
| Variable | Required | Default | Purpose |
|
|
158
|
+
| --- | --- | --- | --- |
|
|
159
|
+
| `FLUXPOOL_API_KEY` | yes | — | Your `fp_live_…` key. |
|
|
160
|
+
| `FLUXPOOL_MCP_URL` | no | `https://api.fluxpool.ai/v1/mcp` | Point at a different backend (local or staging). |
|
|
161
|
+
|
|
162
|
+
## Troubleshooting
|
|
163
|
+
|
|
164
|
+
**"FLUXPOOL_API_KEY is not set"** — the key never reached the process. It goes
|
|
165
|
+
in the `env` block of the server entry, not at the top level of the config.
|
|
166
|
+
|
|
167
|
+
**"Invalid API key"** — the key was rejected. Confirm it is active at
|
|
168
|
+
<https://app.fluxpool.ai/api>; revoked keys fail this way.
|
|
169
|
+
|
|
170
|
+
**The server doesn't appear at all** — check the client's MCP logs. On Claude
|
|
171
|
+
Desktop: `~/Library/Logs/Claude/mcp*.log` (macOS) or `%APPDATA%\Claude\logs\`
|
|
172
|
+
(Windows). Diagnostics from this package are written to stderr and show up
|
|
173
|
+
there. Also confirm `node --version` is 20 or newer.
|
|
174
|
+
|
|
175
|
+
**Insufficient credits** — `create_generation` returns an
|
|
176
|
+
`insufficient_credits` error carrying the required and available amounts. Top
|
|
177
|
+
up at <https://app.fluxpool.ai/credits>.
|
|
178
|
+
|
|
179
|
+
## How it works
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
MCP client ──stdio──▶ @fluxpool/mcp-server ──HTTPS──▶ api.fluxpool.ai/v1/mcp
|
|
183
|
+
(Claude, Cursor) (this package) (tools live here)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Tool definitions live on the server, not in this package, and reach clients
|
|
187
|
+
verbatim via `tools/list`. That is deliberate: the hosted endpoint and this
|
|
188
|
+
bridge expose one surface that cannot drift apart, and new tools become
|
|
189
|
+
available without anyone upgrading a package.
|
|
190
|
+
|
|
191
|
+
## Development
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
npm install
|
|
195
|
+
npm run build # tsc -> dist/
|
|
196
|
+
npm test # builds, then runs offline tests
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Tests stub `fetch` and use a dummy key — they never touch the network.
|
|
200
|
+
|
|
201
|
+
### Releasing
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
npm version patch && git push --follow-tags
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
CI builds, tests, checks the tag matches `package.json`, and publishes via
|
|
208
|
+
npm Trusted Publishing (OIDC) — no npm token is stored in this repo.
|
|
209
|
+
|
|
210
|
+
## Contributing
|
|
211
|
+
|
|
212
|
+
Issues and pull requests: <https://github.com/ZKcandy/fluxpool-mcp-server/issues>
|
|
213
|
+
|
|
214
|
+
Because tools are defined server-side, adding or changing one is a backend
|
|
215
|
+
change rather than a change here. Open an issue describing the tool you need.
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
MIT — see [LICENSE](LICENSE).
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stdio <-> HTTP bridge.
|
|
3
|
+
*
|
|
4
|
+
* MCP clients that only speak stdio (Claude Desktop, Cursor) spawn this
|
|
5
|
+
* process and exchange newline-delimited JSON-RPC over stdin/stdout. Each
|
|
6
|
+
* inbound line is forwarded to Fluxpool's hosted MCP endpoint and the reply
|
|
7
|
+
* written back out.
|
|
8
|
+
*
|
|
9
|
+
* Two rules govern stdout and both are load-bearing:
|
|
10
|
+
* - Exactly one JSON message per line, nothing else. Any stray write
|
|
11
|
+
* corrupts the client's parser, which is why every log goes to stderr.
|
|
12
|
+
* - Notifications (a message with no `id`) get no reply, ever. Answering
|
|
13
|
+
* one is a protocol violation.
|
|
14
|
+
*/
|
|
15
|
+
import type { Readable, Writable } from 'node:stream';
|
|
16
|
+
import { type FluxpoolClient } from './client.js';
|
|
17
|
+
export interface BridgeOptions {
|
|
18
|
+
client: FluxpoolClient;
|
|
19
|
+
input?: Readable;
|
|
20
|
+
output?: Writable;
|
|
21
|
+
/** Defaults to stderr. Never stdout — that channel is the protocol. */
|
|
22
|
+
log?: (message: string) => void;
|
|
23
|
+
}
|
|
24
|
+
export declare class Bridge {
|
|
25
|
+
#private;
|
|
26
|
+
constructor(options: BridgeOptions);
|
|
27
|
+
/** Pump stdin until it closes. Resolves when the stream ends. */
|
|
28
|
+
run(): Promise<void>;
|
|
29
|
+
/** Process one inbound line. Exposed for tests. */
|
|
30
|
+
handleLine(line: string): Promise<void>;
|
|
31
|
+
private write;
|
|
32
|
+
}
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stdio <-> HTTP bridge.
|
|
3
|
+
*
|
|
4
|
+
* MCP clients that only speak stdio (Claude Desktop, Cursor) spawn this
|
|
5
|
+
* process and exchange newline-delimited JSON-RPC over stdin/stdout. Each
|
|
6
|
+
* inbound line is forwarded to Fluxpool's hosted MCP endpoint and the reply
|
|
7
|
+
* written back out.
|
|
8
|
+
*
|
|
9
|
+
* Two rules govern stdout and both are load-bearing:
|
|
10
|
+
* - Exactly one JSON message per line, nothing else. Any stray write
|
|
11
|
+
* corrupts the client's parser, which is why every log goes to stderr.
|
|
12
|
+
* - Notifications (a message with no `id`) get no reply, ever. Answering
|
|
13
|
+
* one is a protocol violation.
|
|
14
|
+
*/
|
|
15
|
+
import { createInterface } from 'node:readline';
|
|
16
|
+
import { errorResponse } from './client.js';
|
|
17
|
+
import { JSON_RPC } from './types.js';
|
|
18
|
+
export class Bridge {
|
|
19
|
+
#client;
|
|
20
|
+
#input;
|
|
21
|
+
#output;
|
|
22
|
+
#log;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.#client = options.client;
|
|
25
|
+
this.#input = options.input ?? process.stdin;
|
|
26
|
+
this.#output = options.output ?? process.stdout;
|
|
27
|
+
this.#log = options.log ?? ((message) => process.stderr.write(`${message}\n`));
|
|
28
|
+
}
|
|
29
|
+
/** Pump stdin until it closes. Resolves when the stream ends. */
|
|
30
|
+
async run() {
|
|
31
|
+
const lines = createInterface({ input: this.#input, crlfDelay: Infinity });
|
|
32
|
+
// Messages are handled sequentially. MCP clients do allow concurrent
|
|
33
|
+
// requests, but ordering keeps stdout writes interleaving-free and the
|
|
34
|
+
// upstream calls are fast enough that head-of-line blocking is not a
|
|
35
|
+
// practical concern for six short request/response tools.
|
|
36
|
+
for await (const line of lines) {
|
|
37
|
+
await this.handleLine(line);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Process one inbound line. Exposed for tests. */
|
|
41
|
+
async handleLine(line) {
|
|
42
|
+
const trimmed = line.trim();
|
|
43
|
+
// Blank lines are framing, not messages. Forwarding one would make the
|
|
44
|
+
// endpoint answer a parse error the client never asked for.
|
|
45
|
+
if (trimmed === '')
|
|
46
|
+
return;
|
|
47
|
+
let message;
|
|
48
|
+
try {
|
|
49
|
+
message = JSON.parse(trimmed);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Can't recover an id from unparseable input, so per JSON-RPC the id
|
|
53
|
+
// is null.
|
|
54
|
+
this.write(errorResponse(null, JSON_RPC.PARSE_ERROR, 'Invalid JSON received by the Fluxpool MCP bridge'));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const id = extractId(message);
|
|
58
|
+
let response;
|
|
59
|
+
try {
|
|
60
|
+
response = await this.#client.send(message, id);
|
|
61
|
+
}
|
|
62
|
+
catch (cause) {
|
|
63
|
+
// The client is written not to throw, so reaching here means a bug
|
|
64
|
+
// rather than an expected failure. Report it instead of dying: killing
|
|
65
|
+
// the process would drop the client's whole session.
|
|
66
|
+
this.#log(`[fluxpool-mcp] unexpected bridge error: ${describe(cause)}`);
|
|
67
|
+
if (id === null)
|
|
68
|
+
return;
|
|
69
|
+
response = errorResponse(id, JSON_RPC.INTERNAL_ERROR, `Bridge error: ${describe(cause)}`);
|
|
70
|
+
}
|
|
71
|
+
if (response === null)
|
|
72
|
+
return; // notification — stay silent
|
|
73
|
+
this.write(response);
|
|
74
|
+
}
|
|
75
|
+
#writeRaw(payload) {
|
|
76
|
+
this.#output.write(`${JSON.stringify(payload)}\n`);
|
|
77
|
+
}
|
|
78
|
+
write(payload) {
|
|
79
|
+
this.#writeRaw(payload);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Pull the id out of an inbound message so a synthesised error can be
|
|
84
|
+
* addressed correctly.
|
|
85
|
+
*
|
|
86
|
+
* Returns null for notifications AND for batches. A batch has no single id;
|
|
87
|
+
* a transport failure mid-batch is reported as an id-less error, which is
|
|
88
|
+
* what the spec prescribes for a request that could not be routed.
|
|
89
|
+
*/
|
|
90
|
+
function extractId(message) {
|
|
91
|
+
if (Array.isArray(message))
|
|
92
|
+
return null;
|
|
93
|
+
if (typeof message !== 'object' || message === null)
|
|
94
|
+
return null;
|
|
95
|
+
const id = message.id;
|
|
96
|
+
if (typeof id === 'string' || typeof id === 'number')
|
|
97
|
+
return id;
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
function describe(cause) {
|
|
101
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for Fluxpool's hosted MCP endpoint.
|
|
3
|
+
*
|
|
4
|
+
* One job: POST a JSON-RPC message and hand back what came out, normalising
|
|
5
|
+
* the cases where the endpoint answers with something that isn't JSON-RPC.
|
|
6
|
+
*
|
|
7
|
+
* Three responses have to be told apart, and the distinction matters because
|
|
8
|
+
* writing the wrong thing to stdout corrupts the client's protocol stream:
|
|
9
|
+
*
|
|
10
|
+
* 1. A JSON-RPC response -> relay verbatim.
|
|
11
|
+
* 2. HTTP 202, empty body -> a notification was accepted. Return null; the
|
|
12
|
+
* caller must stay silent, because notifications take no reply.
|
|
13
|
+
* 3. An auth/HTTP error -> arrives as OpenAI-shaped `{error:{message}}`
|
|
14
|
+
* with no `jsonrpc` field, since auth is checked before the JSON-RPC
|
|
15
|
+
* dispatcher runs. Convert to a proper JSON-RPC error.
|
|
16
|
+
*/
|
|
17
|
+
import { type JsonRpcId, type JsonRpcResponse, type JsonValue, type Outbound } from './types.js';
|
|
18
|
+
export declare const DEFAULT_MCP_URL = "https://api.fluxpool.ai/v1/mcp";
|
|
19
|
+
export interface FluxpoolClientOptions {
|
|
20
|
+
apiKey: string;
|
|
21
|
+
/** Override the endpoint (local backend, staging). Defaults to production. */
|
|
22
|
+
url?: string;
|
|
23
|
+
/** Abort a request after this many ms. Defaults to 10 minutes. */
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
/** Injection seam for tests — defaults to global fetch. */
|
|
26
|
+
fetchImpl?: typeof fetch;
|
|
27
|
+
}
|
|
28
|
+
export declare class FluxpoolClient {
|
|
29
|
+
#private;
|
|
30
|
+
constructor(options: FluxpoolClientOptions);
|
|
31
|
+
get url(): string;
|
|
32
|
+
/**
|
|
33
|
+
* Forward one JSON-RPC message (or batch) upstream.
|
|
34
|
+
*
|
|
35
|
+
* @param message Already-parsed JSON-RPC payload from the client.
|
|
36
|
+
* @param id Id to attach to any error we synthesise. Pass null for
|
|
37
|
+
* notifications so a failure can be dropped rather than
|
|
38
|
+
* sent back as a reply to a message that expects none.
|
|
39
|
+
* @returns The upstream response, or null when there is nothing to send.
|
|
40
|
+
*/
|
|
41
|
+
send(message: JsonValue, id: JsonRpcId): Promise<Outbound | null>;
|
|
42
|
+
}
|
|
43
|
+
export declare function errorResponse(id: JsonRpcId, code: number, message: string, data?: JsonValue): JsonRpcResponse;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for Fluxpool's hosted MCP endpoint.
|
|
3
|
+
*
|
|
4
|
+
* One job: POST a JSON-RPC message and hand back what came out, normalising
|
|
5
|
+
* the cases where the endpoint answers with something that isn't JSON-RPC.
|
|
6
|
+
*
|
|
7
|
+
* Three responses have to be told apart, and the distinction matters because
|
|
8
|
+
* writing the wrong thing to stdout corrupts the client's protocol stream:
|
|
9
|
+
*
|
|
10
|
+
* 1. A JSON-RPC response -> relay verbatim.
|
|
11
|
+
* 2. HTTP 202, empty body -> a notification was accepted. Return null; the
|
|
12
|
+
* caller must stay silent, because notifications take no reply.
|
|
13
|
+
* 3. An auth/HTTP error -> arrives as OpenAI-shaped `{error:{message}}`
|
|
14
|
+
* with no `jsonrpc` field, since auth is checked before the JSON-RPC
|
|
15
|
+
* dispatcher runs. Convert to a proper JSON-RPC error.
|
|
16
|
+
*/
|
|
17
|
+
import { JSON_RPC, isRestError, } from './types.js';
|
|
18
|
+
export const DEFAULT_MCP_URL = 'https://api.fluxpool.ai/v1/mcp';
|
|
19
|
+
/**
|
|
20
|
+
* Video generation is submitted asynchronously, so no single call should be
|
|
21
|
+
* slow — but a client may still be mid-poll when a network hiccup stalls the
|
|
22
|
+
* socket. Ten minutes is generous enough never to fire in normal use while
|
|
23
|
+
* still guaranteeing the process can't hang forever on a dead connection.
|
|
24
|
+
*/
|
|
25
|
+
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
26
|
+
export class FluxpoolClient {
|
|
27
|
+
#apiKey;
|
|
28
|
+
#url;
|
|
29
|
+
#timeoutMs;
|
|
30
|
+
#fetch;
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.#apiKey = options.apiKey;
|
|
33
|
+
this.#url = options.url ?? DEFAULT_MCP_URL;
|
|
34
|
+
this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
35
|
+
this.#fetch = options.fetchImpl ?? globalThis.fetch;
|
|
36
|
+
}
|
|
37
|
+
get url() {
|
|
38
|
+
return this.#url;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Forward one JSON-RPC message (or batch) upstream.
|
|
42
|
+
*
|
|
43
|
+
* @param message Already-parsed JSON-RPC payload from the client.
|
|
44
|
+
* @param id Id to attach to any error we synthesise. Pass null for
|
|
45
|
+
* notifications so a failure can be dropped rather than
|
|
46
|
+
* sent back as a reply to a message that expects none.
|
|
47
|
+
* @returns The upstream response, or null when there is nothing to send.
|
|
48
|
+
*/
|
|
49
|
+
async send(message, id) {
|
|
50
|
+
const controller = new AbortController();
|
|
51
|
+
const timer = setTimeout(() => controller.abort(), this.#timeoutMs);
|
|
52
|
+
let response;
|
|
53
|
+
try {
|
|
54
|
+
response = await this.#fetch(this.#url, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: {
|
|
57
|
+
'Content-Type': 'application/json',
|
|
58
|
+
Accept: 'application/json',
|
|
59
|
+
Authorization: `Bearer ${this.#apiKey}`,
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(message),
|
|
62
|
+
signal: controller.signal,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (cause) {
|
|
66
|
+
// Network failure, DNS, TLS, or our own abort. A notification has no
|
|
67
|
+
// reply channel, so there is nothing useful to return.
|
|
68
|
+
if (id === null)
|
|
69
|
+
return null;
|
|
70
|
+
const reason = cause instanceof Error && cause.name === 'AbortError'
|
|
71
|
+
? `Request timed out after ${this.#timeoutMs}ms`
|
|
72
|
+
: `Could not reach ${this.#url}: ${describe(cause)}`;
|
|
73
|
+
return errorResponse(id, JSON_RPC.TRANSPORT_ERROR, reason);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
}
|
|
78
|
+
const text = (await response.text()).trim();
|
|
79
|
+
// 202 + empty body: a notification was accepted. Nothing to relay.
|
|
80
|
+
if (text === '')
|
|
81
|
+
return null;
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(text);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
if (id === null)
|
|
88
|
+
return null;
|
|
89
|
+
return errorResponse(id, JSON_RPC.TRANSPORT_ERROR, `Fluxpool returned a non-JSON response (HTTP ${response.status}): ${truncate(text)}`);
|
|
90
|
+
}
|
|
91
|
+
// Auth rejections and other pre-dispatch failures arrive in the REST
|
|
92
|
+
// error shape. Translate so the client sees valid JSON-RPC.
|
|
93
|
+
if (!response.ok && isRestError(parsed)) {
|
|
94
|
+
if (id === null)
|
|
95
|
+
return null;
|
|
96
|
+
const code = response.status === 401 || response.status === 403
|
|
97
|
+
? JSON_RPC.AUTH_ERROR
|
|
98
|
+
: JSON_RPC.TRANSPORT_ERROR;
|
|
99
|
+
const hint = code === JSON_RPC.AUTH_ERROR
|
|
100
|
+
? ' Check that FLUXPOOL_API_KEY is a valid key from https://app.fluxpool.ai/api'
|
|
101
|
+
: '';
|
|
102
|
+
return errorResponse(id, code, `${parsed.error.message}${hint}`, {
|
|
103
|
+
http_status: response.status,
|
|
104
|
+
code: parsed.error.code ?? null,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
// Anything else that isn't JSON-RPC — an unexpected gateway body, say.
|
|
108
|
+
if (!response.ok && !isPassThrough(parsed)) {
|
|
109
|
+
if (id === null)
|
|
110
|
+
return null;
|
|
111
|
+
return errorResponse(id, JSON_RPC.TRANSPORT_ERROR, `Fluxpool returned HTTP ${response.status}: ${truncate(text)}`);
|
|
112
|
+
}
|
|
113
|
+
return parsed;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** JSON-RPC responses and batches thereof pass through untouched. */
|
|
117
|
+
function isPassThrough(value) {
|
|
118
|
+
if (Array.isArray(value))
|
|
119
|
+
return true;
|
|
120
|
+
return (typeof value === 'object' &&
|
|
121
|
+
value !== null &&
|
|
122
|
+
value.jsonrpc === '2.0');
|
|
123
|
+
}
|
|
124
|
+
export function errorResponse(id, code, message, data) {
|
|
125
|
+
const error = { code, message };
|
|
126
|
+
if (data !== undefined)
|
|
127
|
+
error.data = data;
|
|
128
|
+
return { jsonrpc: '2.0', id, error };
|
|
129
|
+
}
|
|
130
|
+
function describe(cause) {
|
|
131
|
+
if (cause instanceof Error)
|
|
132
|
+
return cause.message;
|
|
133
|
+
return String(cause);
|
|
134
|
+
}
|
|
135
|
+
function truncate(text, max = 200) {
|
|
136
|
+
return text.length <= max ? text : `${text.slice(0, max)}...`;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @fluxpool/mcp-server — entry point.
|
|
4
|
+
*
|
|
5
|
+
* Reads FLUXPOOL_API_KEY from the environment, opens the stdio bridge to
|
|
6
|
+
* Fluxpool's hosted MCP endpoint, and relays until stdin closes.
|
|
7
|
+
*
|
|
8
|
+
* Tools are NOT defined here. They are served by the backend and reach the
|
|
9
|
+
* client verbatim through `tools/list`, so the two MCP surfaces — hosted
|
|
10
|
+
* HTTP and this local stdio bridge — can never drift apart.
|
|
11
|
+
*/
|
|
12
|
+
export declare function main(env?: NodeJS.ProcessEnv): Promise<number>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @fluxpool/mcp-server — entry point.
|
|
4
|
+
*
|
|
5
|
+
* Reads FLUXPOOL_API_KEY from the environment, opens the stdio bridge to
|
|
6
|
+
* Fluxpool's hosted MCP endpoint, and relays until stdin closes.
|
|
7
|
+
*
|
|
8
|
+
* Tools are NOT defined here. They are served by the backend and reach the
|
|
9
|
+
* client verbatim through `tools/list`, so the two MCP surfaces — hosted
|
|
10
|
+
* HTTP and this local stdio bridge — can never drift apart.
|
|
11
|
+
*/
|
|
12
|
+
import { Bridge } from './bridge.js';
|
|
13
|
+
import { pathToFileURL } from 'node:url';
|
|
14
|
+
import { DEFAULT_MCP_URL, FluxpoolClient } from './client.js';
|
|
15
|
+
/** Log to stderr. stdout carries the protocol and must stay clean. */
|
|
16
|
+
function log(message) {
|
|
17
|
+
process.stderr.write(`${message}\n`);
|
|
18
|
+
}
|
|
19
|
+
export async function main(env = process.env) {
|
|
20
|
+
const apiKey = env.FLUXPOOL_API_KEY?.trim();
|
|
21
|
+
if (!apiKey) {
|
|
22
|
+
log([
|
|
23
|
+
'fluxpool-mcp: FLUXPOOL_API_KEY is not set.',
|
|
24
|
+
'',
|
|
25
|
+
'Create a key at https://app.fluxpool.ai/api, then add it to your MCP',
|
|
26
|
+
'client config. For Claude Desktop that means an "env" block:',
|
|
27
|
+
'',
|
|
28
|
+
' "mcpServers": {',
|
|
29
|
+
' "fluxpool": {',
|
|
30
|
+
' "command": "npx",',
|
|
31
|
+
' "args": ["-y", "@fluxpool/mcp-server"],',
|
|
32
|
+
' "env": { "FLUXPOOL_API_KEY": "fp_live_..." }',
|
|
33
|
+
' }',
|
|
34
|
+
' }',
|
|
35
|
+
'',
|
|
36
|
+
'See https://github.com/ZKcandy/fluxpool-mcp-server#setup for other clients.',
|
|
37
|
+
].join('\n'));
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
// Warn but continue: a malformed key still reaches the endpoint, and its
|
|
41
|
+
// 401 is more informative than anything guessed here. Key formats also
|
|
42
|
+
// change more often than this package ships.
|
|
43
|
+
if (!apiKey.startsWith('fp_live_')) {
|
|
44
|
+
log('fluxpool-mcp: warning — FLUXPOOL_API_KEY does not start with "fp_live_". Continuing anyway.');
|
|
45
|
+
}
|
|
46
|
+
const url = env.FLUXPOOL_MCP_URL?.trim() || DEFAULT_MCP_URL;
|
|
47
|
+
const client = new FluxpoolClient({ apiKey, url });
|
|
48
|
+
if (url !== DEFAULT_MCP_URL)
|
|
49
|
+
log(`fluxpool-mcp: using endpoint ${url}`);
|
|
50
|
+
await new Bridge({ client, log }).run();
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
// Run only when executed directly, so tests can import main() freely.
|
|
54
|
+
// process.argv[1] is the resolved script path; compare against this module.
|
|
55
|
+
const invokedDirectly = process.argv[1] !== undefined &&
|
|
56
|
+
import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
57
|
+
if (invokedDirectly) {
|
|
58
|
+
main()
|
|
59
|
+
.then((code) => {
|
|
60
|
+
process.exitCode = code;
|
|
61
|
+
})
|
|
62
|
+
.catch((cause) => {
|
|
63
|
+
log(`fluxpool-mcp: fatal — ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;GASG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE9D,sEAAsE;AACtE,SAAS,GAAG,CAAC,OAAe;IAC1B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7D,MAAM,MAAM,GAAG,GAAG,CAAC,gBAAgB,EAAE,IAAI,EAAE,CAAC;IAE5C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,GAAG,CACD;YACE,4CAA4C;YAC5C,EAAE;YACF,sEAAsE;YACtE,8DAA8D;YAC9D,EAAE;YACF,mBAAmB;YACnB,mBAAmB;YACnB,yBAAyB;YACzB,+CAA+C;YAC/C,oDAAoD;YACpD,OAAO;YACP,KAAK;YACL,EAAE;YACF,6EAA6E;SAC9E,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAC;QACF,OAAO,CAAC,CAAC;IACX,CAAC;IAED,yEAAyE;IACzE,uEAAuE;IACvE,6CAA6C;IAC7C,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACnC,GAAG,CAAC,6FAA6F,CAAC,CAAC;IACrG,CAAC;IAED,MAAM,GAAG,GAAG,GAAG,CAAC,gBAAgB,EAAE,IAAI,EAAE,IAAI,eAAe,CAAC;IAC5D,MAAM,MAAM,GAAG,IAAI,cAAc,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;IAEnD,IAAI,GAAG,KAAK,eAAe;QAAE,GAAG,CAAC,gCAAgC,GAAG,EAAE,CAAC,CAAC;IAExE,MAAM,IAAI,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC;IACxC,OAAO,CAAC,CAAC;AACX,CAAC;AAED,sEAAsE;AACtE,4EAA4E;AAC5E,MAAM,eAAe,GACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS;IAC7B,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAE1D,IAAI,eAAe,EAAE,CAAC;IACpB,IAAI,EAAE;SACH,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;QACb,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAC1B,CAAC,CAAC;SACD,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;QACxB,GAAG,CAAC,yBAAyB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACvF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACvB,CAAC,CAAC,CAAC;AACP,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC 2.0 + Fluxpool REST types.
|
|
3
|
+
*
|
|
4
|
+
* This package is a transport bridge, not a tool implementation: it relays
|
|
5
|
+
* JSON-RPC messages between a stdio MCP client and Fluxpool's hosted MCP
|
|
6
|
+
* endpoint. So the types here describe the *envelope*, not the tools. Tool
|
|
7
|
+
* schemas live on the backend (`functions/ingestion/index.js`) and reach
|
|
8
|
+
* clients verbatim via `tools/list` — deliberately, so the two surfaces can
|
|
9
|
+
* never drift.
|
|
10
|
+
*/
|
|
11
|
+
/** A JSON value, as far as JSON-RPC is concerned. */
|
|
12
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
13
|
+
[key: string]: JsonValue;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* JSON-RPC request id. Per spec this is a string, number, or null.
|
|
17
|
+
* A message with NO id is a notification and must not be replied to.
|
|
18
|
+
*/
|
|
19
|
+
export type JsonRpcId = string | number | null;
|
|
20
|
+
/** A single JSON-RPC 2.0 request or notification travelling client -> server. */
|
|
21
|
+
export type JsonRpcRequest = {
|
|
22
|
+
jsonrpc: '2.0';
|
|
23
|
+
method: string;
|
|
24
|
+
/** Absent means notification: the server must stay silent. */
|
|
25
|
+
id?: JsonRpcId;
|
|
26
|
+
params?: JsonValue;
|
|
27
|
+
};
|
|
28
|
+
/** A single JSON-RPC 2.0 response travelling server -> client. */
|
|
29
|
+
export type JsonRpcResponse = {
|
|
30
|
+
jsonrpc: '2.0';
|
|
31
|
+
id: JsonRpcId;
|
|
32
|
+
result?: JsonValue;
|
|
33
|
+
error?: JsonRpcErrorObject;
|
|
34
|
+
};
|
|
35
|
+
export type JsonRpcErrorObject = {
|
|
36
|
+
code: number;
|
|
37
|
+
message: string;
|
|
38
|
+
data?: JsonValue;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Standard JSON-RPC error codes, plus the implementation-defined range we
|
|
42
|
+
* use for transport failures. -32000..-32099 is reserved for the server to
|
|
43
|
+
* define, which is where an HTTP-layer problem belongs: the request never
|
|
44
|
+
* reached a method handler.
|
|
45
|
+
*/
|
|
46
|
+
export declare const JSON_RPC: {
|
|
47
|
+
readonly PARSE_ERROR: -32700;
|
|
48
|
+
readonly INVALID_REQUEST: -32600;
|
|
49
|
+
readonly METHOD_NOT_FOUND: -32601;
|
|
50
|
+
readonly INVALID_PARAMS: -32602;
|
|
51
|
+
readonly INTERNAL_ERROR: -32603;
|
|
52
|
+
/** Transport-level failure talking to the Fluxpool endpoint. */
|
|
53
|
+
readonly TRANSPORT_ERROR: -32001;
|
|
54
|
+
/** Auth rejected (bad or missing API key). */
|
|
55
|
+
readonly AUTH_ERROR: -32002;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Fluxpool's REST error envelope. The hosted MCP endpoint returns JSON-RPC
|
|
59
|
+
* for anything that reaches the dispatcher, but auth is checked *before*
|
|
60
|
+
* that — so a 401 arrives in this OpenAI-compatible shape with no `jsonrpc`
|
|
61
|
+
* field at all. The bridge has to recognise and translate it.
|
|
62
|
+
*/
|
|
63
|
+
export interface FluxpoolRestError {
|
|
64
|
+
error: {
|
|
65
|
+
message: string;
|
|
66
|
+
type?: string;
|
|
67
|
+
param?: string | null;
|
|
68
|
+
code?: string;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Narrowing helper for the REST error shape above. */
|
|
72
|
+
export declare function isRestError(value: unknown): value is FluxpoolRestError;
|
|
73
|
+
/** True when a parsed payload already looks like JSON-RPC we can pass through. */
|
|
74
|
+
export declare function isJsonRpcMessage(value: unknown): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* What the bridge writes to stdout: either a payload relayed verbatim from
|
|
77
|
+
* upstream (an opaque `JsonValue` — we do not re-validate what the backend
|
|
78
|
+
* sends) or an error this package synthesised. Keeping the union explicit
|
|
79
|
+
* avoids laundering typed shapes through `JsonValue`, which drops the very
|
|
80
|
+
* guarantees the types exist to provide.
|
|
81
|
+
*/
|
|
82
|
+
export type Outbound = JsonValue | JsonRpcResponse;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-RPC 2.0 + Fluxpool REST types.
|
|
3
|
+
*
|
|
4
|
+
* This package is a transport bridge, not a tool implementation: it relays
|
|
5
|
+
* JSON-RPC messages between a stdio MCP client and Fluxpool's hosted MCP
|
|
6
|
+
* endpoint. So the types here describe the *envelope*, not the tools. Tool
|
|
7
|
+
* schemas live on the backend (`functions/ingestion/index.js`) and reach
|
|
8
|
+
* clients verbatim via `tools/list` — deliberately, so the two surfaces can
|
|
9
|
+
* never drift.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Standard JSON-RPC error codes, plus the implementation-defined range we
|
|
13
|
+
* use for transport failures. -32000..-32099 is reserved for the server to
|
|
14
|
+
* define, which is where an HTTP-layer problem belongs: the request never
|
|
15
|
+
* reached a method handler.
|
|
16
|
+
*/
|
|
17
|
+
export const JSON_RPC = {
|
|
18
|
+
PARSE_ERROR: -32700,
|
|
19
|
+
INVALID_REQUEST: -32600,
|
|
20
|
+
METHOD_NOT_FOUND: -32601,
|
|
21
|
+
INVALID_PARAMS: -32602,
|
|
22
|
+
INTERNAL_ERROR: -32603,
|
|
23
|
+
/** Transport-level failure talking to the Fluxpool endpoint. */
|
|
24
|
+
TRANSPORT_ERROR: -32001,
|
|
25
|
+
/** Auth rejected (bad or missing API key). */
|
|
26
|
+
AUTH_ERROR: -32002,
|
|
27
|
+
};
|
|
28
|
+
/** Narrowing helper for the REST error shape above. */
|
|
29
|
+
export function isRestError(value) {
|
|
30
|
+
if (typeof value !== 'object' || value === null)
|
|
31
|
+
return false;
|
|
32
|
+
const err = value.error;
|
|
33
|
+
if (typeof err !== 'object' || err === null)
|
|
34
|
+
return false;
|
|
35
|
+
return typeof err.message === 'string';
|
|
36
|
+
}
|
|
37
|
+
/** True when a parsed payload already looks like JSON-RPC we can pass through. */
|
|
38
|
+
export function isJsonRpcMessage(value) {
|
|
39
|
+
if (typeof value !== 'object' || value === null)
|
|
40
|
+
return false;
|
|
41
|
+
return value.jsonrpc === '2.0';
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAwCH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,WAAW,EAAE,CAAC,KAAK;IACnB,eAAe,EAAE,CAAC,KAAK;IACvB,gBAAgB,EAAE,CAAC,KAAK;IACxB,cAAc,EAAE,CAAC,KAAK;IACtB,cAAc,EAAE,CAAC,KAAK;IACtB,gEAAgE;IAChE,eAAe,EAAE,CAAC,KAAK;IACvB,8CAA8C;IAC9C,UAAU,EAAE,CAAC,KAAK;CACV,CAAC;AAiBX,uDAAuD;AACvD,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,GAAG,GAAI,KAA6B,CAAC,KAAK,CAAC;IACjD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC1D,OAAO,OAAQ,GAA6B,CAAC,OAAO,KAAK,QAAQ,CAAC;AACpE,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC9D,OAAQ,KAA+B,CAAC,OAAO,KAAK,KAAK,CAAC;AAC5D,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fluxpool/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Fluxpool.ai — generate images and videos across Flux, Seedream, Seedance, Runway and more from Claude, Cursor, or any MCP client.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"fluxpool",
|
|
9
|
+
"ai",
|
|
10
|
+
"image-generation",
|
|
11
|
+
"video-generation",
|
|
12
|
+
"claude",
|
|
13
|
+
"cursor"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://fluxpool.ai",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/ZKcandy/fluxpool-mcp-server/issues"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/ZKcandy/fluxpool-mcp-server.git"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Fluxpool",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"bin": {
|
|
27
|
+
"fluxpool-mcp": "dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc",
|
|
39
|
+
"prepublishOnly": "npm run build && npm test",
|
|
40
|
+
"start": "node dist/index.js",
|
|
41
|
+
"test": "npm run build && node --test test/"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.0.4"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^22.10.2",
|
|
48
|
+
"typescript": "^5.7.2"
|
|
49
|
+
}
|
|
50
|
+
}
|