@nexdoc/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/README.md +45 -0
- package/dist/client.d.ts +61 -0
- package/dist/client.js +121 -0
- package/dist/context.d.ts +7 -0
- package/dist/context.js +14 -0
- package/dist/formats.d.ts +6 -0
- package/dist/formats.js +39 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +72 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +10 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +179 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @nexdoc/mcp-server
|
|
2
|
+
|
|
3
|
+
Model Context Protocol server for [NexDoc Design](https://www.nexdoc.design).
|
|
4
|
+
|
|
5
|
+
## Transports
|
|
6
|
+
|
|
7
|
+
| Mode | Command | Auth |
|
|
8
|
+
|------|---------|------|
|
|
9
|
+
| **stdio** (default) | `npx -y @nexdoc/mcp-server` | `NXD_API_KEY` |
|
|
10
|
+
| **HTTP** | `npx -y @nexdoc/mcp-server --http --port 8084` | `Authorization: Bearer` (OAuth `nxd_at_…` or API key) |
|
|
11
|
+
|
|
12
|
+
## Claude Desktop / Cursor (stdio)
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"nexdoc": {
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": ["-y", "@nexdoc/mcp-server"],
|
|
20
|
+
"env": {
|
|
21
|
+
"NXD_API_KEY": "nxd_live_...",
|
|
22
|
+
"NXD_API_URL": "https://api.nexdoc.design"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Tools
|
|
30
|
+
|
|
31
|
+
`create_design`, `update_design`, `check_run`, `refresh_preview`, `wait_for_run`, `list_runs`, `export_design`, `publish_design`, `unpublish_design`, `list_formats`, `get_balance`
|
|
32
|
+
|
|
33
|
+
## Develop
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install
|
|
37
|
+
npm run build
|
|
38
|
+
NXD_API_KEY=nxd_test_... NXD_API_URL=http://127.0.0.1:8082 node dist/index.js
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Docs: [../../docs/agents/mcp.md](../../docs/agents/mcp.md)
|
|
42
|
+
|
|
43
|
+
## TODO
|
|
44
|
+
|
|
45
|
+
- [ ] **Publish `@nexdoc/mcp-server` to npm** after MCP stdio + HTTP tests pass (Inspector, compose `mcp` service, OAuth discovery). Until then, clients should run from this repo (`node dist/index.js` or Docker), not `npx -y @nexdoc/mcp-server`.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export declare class ApiError extends Error {
|
|
2
|
+
status: number;
|
|
3
|
+
detail: string;
|
|
4
|
+
constructor(status: number, detail: string);
|
|
5
|
+
}
|
|
6
|
+
export type Job = {
|
|
7
|
+
job_id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
10
|
+
export type RunCreated = {
|
|
11
|
+
job_id: string;
|
|
12
|
+
run_id: string;
|
|
13
|
+
status: string;
|
|
14
|
+
created_at: string;
|
|
15
|
+
};
|
|
16
|
+
export type RunStatus = {
|
|
17
|
+
run_id: string;
|
|
18
|
+
job_id?: string;
|
|
19
|
+
status: string;
|
|
20
|
+
charge_usd?: number;
|
|
21
|
+
viewer_url?: string;
|
|
22
|
+
commit_hash?: string;
|
|
23
|
+
error?: string;
|
|
24
|
+
log_tail?: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function createJob(name: string): Promise<Job>;
|
|
27
|
+
export declare function listJobs(): Promise<{
|
|
28
|
+
data: Job[];
|
|
29
|
+
}>;
|
|
30
|
+
export declare function createRunJson(jobId: string, body: {
|
|
31
|
+
format?: string;
|
|
32
|
+
instructions?: string;
|
|
33
|
+
content?: string;
|
|
34
|
+
files?: string[];
|
|
35
|
+
assets?: Array<{
|
|
36
|
+
name: string;
|
|
37
|
+
file_id: string;
|
|
38
|
+
}>;
|
|
39
|
+
webhook_url?: string;
|
|
40
|
+
}): Promise<RunCreated>;
|
|
41
|
+
export declare function createRunMultipart(jobId: string, opts: {
|
|
42
|
+
format?: string;
|
|
43
|
+
instructions?: string;
|
|
44
|
+
content?: string;
|
|
45
|
+
contentPath?: string;
|
|
46
|
+
assetPaths?: string[];
|
|
47
|
+
}): Promise<RunCreated>;
|
|
48
|
+
export declare function getRun(jobId: string, runId: string): Promise<RunStatus>;
|
|
49
|
+
export declare function listRuns(jobId: string): Promise<{
|
|
50
|
+
job_id: string;
|
|
51
|
+
data: RunStatus[];
|
|
52
|
+
}>;
|
|
53
|
+
export declare function exportDesign(jobId: string, format: "pdf" | "html", runId?: string): Promise<Record<string, unknown>>;
|
|
54
|
+
export declare function publishDesign(jobId: string, runId?: string): Promise<Record<string, unknown>>;
|
|
55
|
+
export declare function unpublishDesign(jobId: string): Promise<Record<string, unknown>>;
|
|
56
|
+
export declare function getPreviewLink(jobId: string): Promise<Record<string, unknown>>;
|
|
57
|
+
export declare function getBalance(): Promise<Record<string, unknown>>;
|
|
58
|
+
export declare function waitForRun(jobId: string, runId: string, opts?: {
|
|
59
|
+
timeoutSec?: number;
|
|
60
|
+
intervalSec?: number;
|
|
61
|
+
}): Promise<RunStatus>;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getApiBase, getBearer } from "./context.js";
|
|
4
|
+
export class ApiError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
detail;
|
|
7
|
+
constructor(status, detail) {
|
|
8
|
+
super(detail);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.detail = detail;
|
|
11
|
+
this.name = "ApiError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function request(method, apiPath, init = {}) {
|
|
15
|
+
const headers = new Headers();
|
|
16
|
+
headers.set("Authorization", `Bearer ${getBearer()}`);
|
|
17
|
+
let body;
|
|
18
|
+
if (init.form) {
|
|
19
|
+
body = init.form;
|
|
20
|
+
}
|
|
21
|
+
else if (init.json !== undefined) {
|
|
22
|
+
headers.set("Content-Type", "application/json");
|
|
23
|
+
body = JSON.stringify(init.json);
|
|
24
|
+
}
|
|
25
|
+
const res = await fetch(`${getApiBase()}${apiPath}`, { method, headers, body });
|
|
26
|
+
const text = await res.text();
|
|
27
|
+
let parsed = null;
|
|
28
|
+
if (text) {
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(text);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
parsed = { detail: text };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
const detail = typeof parsed === "object" && parsed && "detail" in parsed
|
|
38
|
+
? String(parsed.detail)
|
|
39
|
+
: res.statusText;
|
|
40
|
+
throw new ApiError(res.status, detail);
|
|
41
|
+
}
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
export async function createJob(name) {
|
|
45
|
+
return request("POST", "/v1/jobs", { json: { name } });
|
|
46
|
+
}
|
|
47
|
+
export async function listJobs() {
|
|
48
|
+
return request("GET", "/v1/jobs");
|
|
49
|
+
}
|
|
50
|
+
export async function createRunJson(jobId, body) {
|
|
51
|
+
return request("POST", `/v1/jobs/${jobId}/runs`, { json: body });
|
|
52
|
+
}
|
|
53
|
+
export async function createRunMultipart(jobId, opts) {
|
|
54
|
+
const form = new FormData();
|
|
55
|
+
if (opts.format)
|
|
56
|
+
form.set("format", opts.format);
|
|
57
|
+
if (opts.instructions)
|
|
58
|
+
form.set("instructions", opts.instructions);
|
|
59
|
+
if (opts.contentPath) {
|
|
60
|
+
const buf = await readFile(opts.contentPath);
|
|
61
|
+
const name = path.basename(opts.contentPath);
|
|
62
|
+
form.append("content", new Blob([buf]), name);
|
|
63
|
+
}
|
|
64
|
+
else if (opts.content !== undefined) {
|
|
65
|
+
form.set("content", opts.content);
|
|
66
|
+
}
|
|
67
|
+
for (const assetPath of opts.assetPaths || []) {
|
|
68
|
+
const buf = await readFile(assetPath);
|
|
69
|
+
const name = path.basename(assetPath);
|
|
70
|
+
const lower = name.toLowerCase();
|
|
71
|
+
const type = lower.endsWith(".png")
|
|
72
|
+
? "image/png"
|
|
73
|
+
: lower.endsWith(".jpg") || lower.endsWith(".jpeg")
|
|
74
|
+
? "image/jpeg"
|
|
75
|
+
: lower.endsWith(".gif")
|
|
76
|
+
? "image/gif"
|
|
77
|
+
: lower.endsWith(".pdf")
|
|
78
|
+
? "application/pdf"
|
|
79
|
+
: "application/octet-stream";
|
|
80
|
+
form.append("files", new Blob([buf], { type }), name);
|
|
81
|
+
}
|
|
82
|
+
return request("POST", `/v1/jobs/${jobId}/runs`, { form });
|
|
83
|
+
}
|
|
84
|
+
export async function getRun(jobId, runId) {
|
|
85
|
+
return request("GET", `/v1/jobs/${jobId}/runs/${runId}`);
|
|
86
|
+
}
|
|
87
|
+
export async function listRuns(jobId) {
|
|
88
|
+
return request("GET", `/v1/jobs/${jobId}/runs`);
|
|
89
|
+
}
|
|
90
|
+
export async function exportDesign(jobId, format, runId) {
|
|
91
|
+
return request("POST", `/v1/jobs/${jobId}/export`, {
|
|
92
|
+
json: { format, run_id: runId ?? null },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
export async function publishDesign(jobId, runId) {
|
|
96
|
+
return request("POST", `/v1/jobs/${jobId}/publish`, {
|
|
97
|
+
json: { run_id: runId ?? null },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
export async function unpublishDesign(jobId) {
|
|
101
|
+
return request("POST", `/v1/jobs/${jobId}/unpublish`, { json: {} });
|
|
102
|
+
}
|
|
103
|
+
export async function getPreviewLink(jobId) {
|
|
104
|
+
return request("GET", `/v1/jobs/${jobId}/viewer/session`);
|
|
105
|
+
}
|
|
106
|
+
export async function getBalance() {
|
|
107
|
+
return request("GET", "/v1/credits");
|
|
108
|
+
}
|
|
109
|
+
export async function waitForRun(jobId, runId, opts = {}) {
|
|
110
|
+
const timeoutSec = opts.timeoutSec ?? 900;
|
|
111
|
+
const intervalSec = opts.intervalSec ?? 5;
|
|
112
|
+
const deadline = Date.now() + timeoutSec * 1000;
|
|
113
|
+
let last = null;
|
|
114
|
+
while (Date.now() < deadline) {
|
|
115
|
+
last = await getRun(jobId, runId);
|
|
116
|
+
if (["completed", "failed", "cancelled"].includes(last.status))
|
|
117
|
+
return last;
|
|
118
|
+
await new Promise((r) => setTimeout(r, intervalSec * 1000));
|
|
119
|
+
}
|
|
120
|
+
throw new Error(`Timed out after ${timeoutSec}s waiting for ${runId} (last status: ${last?.status ?? "unknown"})`);
|
|
121
|
+
}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
export const authStore = new AsyncLocalStorage();
|
|
3
|
+
export function getBearer() {
|
|
4
|
+
const fromStore = authStore.getStore()?.bearer;
|
|
5
|
+
if (fromStore)
|
|
6
|
+
return fromStore;
|
|
7
|
+
const envKey = process.env.NXD_API_KEY?.trim();
|
|
8
|
+
if (envKey)
|
|
9
|
+
return envKey;
|
|
10
|
+
throw new Error("No auth token. Set NXD_API_KEY for stdio mode, or send Authorization: Bearer for HTTP mode.");
|
|
11
|
+
}
|
|
12
|
+
export function getApiBase() {
|
|
13
|
+
return (process.env.NXD_API_URL || "https://api.nexdoc.design").replace(/\/$/, "");
|
|
14
|
+
}
|
package/dist/formats.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const FORMATS = [
|
|
2
|
+
{ slug: "landing-page", family: "web", when: "Marketing / product homepage" },
|
|
3
|
+
{ slug: "link-in-bio", family: "web", when: "Compact link hub" },
|
|
4
|
+
{ slug: "portfolio", family: "web", when: "Personal or studio portfolio" },
|
|
5
|
+
{ slug: "lookbook", family: "web", when: "Visual product/collection story" },
|
|
6
|
+
{ slug: "slide-deck", family: "slides", when: "General presentation (Reveal.js)" },
|
|
7
|
+
{ slug: "presentation", family: "slides", when: "Alias of slide-deck" },
|
|
8
|
+
{ slug: "pitch-deck", family: "slides", when: "Fundraising / sales narrative" },
|
|
9
|
+
{ slug: "report", family: "print", when: "Multi-page narrative with figures" },
|
|
10
|
+
{ slug: "proposal", family: "print", when: "Business proposal" },
|
|
11
|
+
{ slug: "case-study", family: "print", when: "Case study document" },
|
|
12
|
+
{ slug: "whitepaper", family: "print", when: "Long-form whitepaper" },
|
|
13
|
+
{ slug: "invoice", family: "print", when: "Structured invoice" },
|
|
14
|
+
{ slug: "receipt", family: "print", when: "Receipt" },
|
|
15
|
+
{ slug: "resume", family: "print", when: "Resume" },
|
|
16
|
+
{ slug: "cv", family: "print", when: "CV" },
|
|
17
|
+
{ slug: "cover-letter", family: "print", when: "Cover letter" },
|
|
18
|
+
{ slug: "contract", family: "print", when: "Contract" },
|
|
19
|
+
{ slug: "nda", family: "print", when: "NDA" },
|
|
20
|
+
{ slug: "certificate", family: "print", when: "Certificate / award" },
|
|
21
|
+
{ slug: "brochure", family: "print", when: "Brochure" },
|
|
22
|
+
{ slug: "menu", family: "print", when: "Menu" },
|
|
23
|
+
{ slug: "business-card", family: "fixed-canvas", when: "Contact card" },
|
|
24
|
+
{ slug: "social-card", family: "fixed-canvas", when: "Social share card" },
|
|
25
|
+
{ slug: "og-image", family: "fixed-canvas", when: "Open Graph image" },
|
|
26
|
+
{ slug: "banner", family: "fixed-canvas", when: "Banner" },
|
|
27
|
+
{ slug: "poster", family: "fixed-canvas", when: "Poster" },
|
|
28
|
+
{ slug: "invitation", family: "fixed-canvas", when: "Invitation" },
|
|
29
|
+
{ slug: "ticket", family: "fixed-canvas", when: "Ticket" },
|
|
30
|
+
{ slug: "boarding-pass", family: "fixed-canvas", when: "Boarding pass" },
|
|
31
|
+
{ slug: "coupon", family: "fixed-canvas", when: "Coupon" },
|
|
32
|
+
{ slug: "voucher", family: "fixed-canvas", when: "Voucher" },
|
|
33
|
+
{ slug: "email-newsletter", family: "other", when: "HTML email layout" },
|
|
34
|
+
{ slug: "infographic", family: "other", when: "Infographic" },
|
|
35
|
+
{ slug: "timeline", family: "other", when: "Timeline" },
|
|
36
|
+
{ slug: "roadmap", family: "other", when: "Roadmap" },
|
|
37
|
+
{ slug: "org-chart", family: "other", when: "Org chart" },
|
|
38
|
+
{ slug: "dashboard", family: "other", when: "Dashboard layout" },
|
|
39
|
+
];
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import express from "express";
|
|
5
|
+
import { authStore } from "./context.js";
|
|
6
|
+
import { getApiBase } from "./context.js";
|
|
7
|
+
import { createServer } from "./server.js";
|
|
8
|
+
function parseArgs(argv) {
|
|
9
|
+
const http = argv.includes("--http");
|
|
10
|
+
const portIdx = argv.indexOf("--port");
|
|
11
|
+
const port = portIdx >= 0 ? Number(argv[portIdx + 1]) : Number(process.env.PORT || 8084);
|
|
12
|
+
return { http, port: Number.isFinite(port) ? port : 8084 };
|
|
13
|
+
}
|
|
14
|
+
async function runStdio() {
|
|
15
|
+
const server = createServer();
|
|
16
|
+
const transport = new StdioServerTransport();
|
|
17
|
+
await server.connect(transport);
|
|
18
|
+
console.error("NexDoc MCP server running on stdio");
|
|
19
|
+
}
|
|
20
|
+
async function runHttp(port) {
|
|
21
|
+
const app = express();
|
|
22
|
+
app.use(express.json({ limit: "4mb" }));
|
|
23
|
+
const apiBase = getApiBase();
|
|
24
|
+
const resourceMetadata = {
|
|
25
|
+
resource: process.env.NXD_MCP_RESOURCE_URL || `http://127.0.0.1:${port}`,
|
|
26
|
+
authorization_servers: [apiBase],
|
|
27
|
+
scopes_supported: ["files:rw", "jobs:rw", "runs:rw"],
|
|
28
|
+
bearer_methods_supported: ["header"],
|
|
29
|
+
};
|
|
30
|
+
app.get("/.well-known/oauth-protected-resource", (_req, res) => {
|
|
31
|
+
res.json(resourceMetadata);
|
|
32
|
+
});
|
|
33
|
+
app.get("/health", (_req, res) => {
|
|
34
|
+
res.json({ ok: true, transport: "http", api: apiBase });
|
|
35
|
+
});
|
|
36
|
+
app.all("/mcp", async (req, res) => {
|
|
37
|
+
const auth = req.header("authorization") || "";
|
|
38
|
+
const bearer = auth.toLowerCase().startsWith("bearer ") ? auth.slice(7).trim() : "";
|
|
39
|
+
if (!bearer && !process.env.NXD_API_KEY) {
|
|
40
|
+
res.status(401).json({
|
|
41
|
+
error: "unauthorized",
|
|
42
|
+
detail: "Authorization: Bearer <nxd_at_... or nxd_live_...> required",
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const token = bearer || process.env.NXD_API_KEY.trim();
|
|
47
|
+
await authStore.run({ bearer: token }, async () => {
|
|
48
|
+
const server = createServer();
|
|
49
|
+
const transport = new StreamableHTTPServerTransport({
|
|
50
|
+
sessionIdGenerator: undefined,
|
|
51
|
+
});
|
|
52
|
+
await server.connect(transport);
|
|
53
|
+
await transport.handleRequest(req, res, req.body);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
app.listen(port, () => {
|
|
57
|
+
console.error(`NexDoc MCP HTTP server on :${port} (API ${apiBase})`);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async function main() {
|
|
61
|
+
const { http, port } = parseArgs(process.argv.slice(2));
|
|
62
|
+
if (http) {
|
|
63
|
+
await runHttp(port);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
await runStdio();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
main().catch((err) => {
|
|
70
|
+
console.error(err);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
});
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { registerTools } from "./tools.js";
|
|
3
|
+
export function createServer() {
|
|
4
|
+
const server = new McpServer({
|
|
5
|
+
name: "nexdoc-design",
|
|
6
|
+
version: "0.1.0",
|
|
7
|
+
});
|
|
8
|
+
registerTools(server);
|
|
9
|
+
return server;
|
|
10
|
+
}
|
package/dist/tools.d.ts
ADDED
package/dist/tools.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as api from "./client.js";
|
|
3
|
+
import { FORMATS } from "./formats.js";
|
|
4
|
+
function ok(data) {
|
|
5
|
+
return {
|
|
6
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function fail(err) {
|
|
10
|
+
const message = err instanceof api.ApiError
|
|
11
|
+
? `API ${err.status}: ${err.detail}`
|
|
12
|
+
: err instanceof Error
|
|
13
|
+
? err.message
|
|
14
|
+
: String(err);
|
|
15
|
+
return {
|
|
16
|
+
content: [{ type: "text", text: message }],
|
|
17
|
+
isError: true,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function registerTools(server) {
|
|
21
|
+
server.tool("list_formats", "List supported NexDoc Design format slugs with selection guidance.", {}, async () => ok({ formats: FORMATS }));
|
|
22
|
+
server.tool("get_balance", "Get the authenticated org wallet summary (USD balances, auto-topup, past_due).", {}, async () => {
|
|
23
|
+
try {
|
|
24
|
+
return ok(await api.getBalance());
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
return fail(err);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
server.tool("create_design", "Create a job (or reuse job_id) and start a generative run. Prefer asset_paths in stdio mode. After completion, return viewer_url and offer export (pdf/html) — do not publish unless the user asks.", {
|
|
31
|
+
name: z.string().optional().describe("Job name when creating a new job"),
|
|
32
|
+
job_id: z.string().optional().describe("Existing job id to reuse"),
|
|
33
|
+
format: z.string().describe("Format slug, e.g. landing-page, report, business-card"),
|
|
34
|
+
instructions: z.string().describe("Design direction / edit notes"),
|
|
35
|
+
content: z.string().optional().describe("Source copy (markdown/text)"),
|
|
36
|
+
content_path: z.string().optional().describe("Local path to content file (stdio)"),
|
|
37
|
+
asset_paths: z
|
|
38
|
+
.array(z.string())
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Local asset file paths to upload (stdio)"),
|
|
41
|
+
wait: z
|
|
42
|
+
.boolean()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("If true, poll until terminal status before returning"),
|
|
45
|
+
timeout_sec: z.number().optional().describe("Wait timeout seconds (default 900)"),
|
|
46
|
+
}, async (args) => {
|
|
47
|
+
try {
|
|
48
|
+
let jobId = args.job_id;
|
|
49
|
+
if (!jobId) {
|
|
50
|
+
const job = await api.createJob(args.name || `Design ${args.format}`);
|
|
51
|
+
jobId = job.job_id;
|
|
52
|
+
}
|
|
53
|
+
const useMultipart = Boolean(args.content_path || (args.asset_paths && args.asset_paths.length));
|
|
54
|
+
const created = useMultipart
|
|
55
|
+
? await api.createRunMultipart(jobId, {
|
|
56
|
+
format: args.format,
|
|
57
|
+
instructions: args.instructions,
|
|
58
|
+
content: args.content,
|
|
59
|
+
contentPath: args.content_path,
|
|
60
|
+
assetPaths: args.asset_paths,
|
|
61
|
+
})
|
|
62
|
+
: await api.createRunJson(jobId, {
|
|
63
|
+
format: args.format,
|
|
64
|
+
instructions: args.instructions,
|
|
65
|
+
content: args.content ?? "",
|
|
66
|
+
});
|
|
67
|
+
if (args.wait) {
|
|
68
|
+
const final = await api.waitForRun(jobId, created.run_id, {
|
|
69
|
+
timeoutSec: args.timeout_sec,
|
|
70
|
+
});
|
|
71
|
+
return ok({ job_id: jobId, run: final });
|
|
72
|
+
}
|
|
73
|
+
return ok({ job_id: jobId, run_id: created.run_id, status: created.status, created_at: created.created_at });
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
return fail(err);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
server.tool("update_design", "Start an update run on an existing job with edit instructions (diff-style).", {
|
|
80
|
+
job_id: z.string(),
|
|
81
|
+
format: z.string().describe("Keep the same format unless changing medium"),
|
|
82
|
+
instructions: z.string().describe("Edit instructions against the current design"),
|
|
83
|
+
content: z.string().optional(),
|
|
84
|
+
wait: z.boolean().optional(),
|
|
85
|
+
timeout_sec: z.number().optional(),
|
|
86
|
+
}, async (args) => {
|
|
87
|
+
try {
|
|
88
|
+
const created = await api.createRunJson(args.job_id, {
|
|
89
|
+
format: args.format,
|
|
90
|
+
instructions: args.instructions,
|
|
91
|
+
content: args.content ?? "",
|
|
92
|
+
});
|
|
93
|
+
if (args.wait) {
|
|
94
|
+
const final = await api.waitForRun(args.job_id, created.run_id, {
|
|
95
|
+
timeoutSec: args.timeout_sec,
|
|
96
|
+
});
|
|
97
|
+
return ok({ ...created, run: final });
|
|
98
|
+
}
|
|
99
|
+
return ok(created);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
return fail(err);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
server.tool("check_run", "Fetch current status for a run (status, charge_usd, viewer_url, error). viewer_url is freshly minted — call this (or refresh_preview) if the user says the preview link expired.", {
|
|
106
|
+
job_id: z.string(),
|
|
107
|
+
run_id: z.string(),
|
|
108
|
+
}, async (args) => {
|
|
109
|
+
try {
|
|
110
|
+
return ok(await api.getRun(args.job_id, args.run_id));
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
return fail(err);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
server.tool("refresh_preview", "Mint a fresh viewer_url for a job. Use when the user says the preview link expired or 401s.", { job_id: z.string() }, async (args) => {
|
|
117
|
+
try {
|
|
118
|
+
return ok(await api.getPreviewLink(args.job_id));
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
return fail(err);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
server.tool("wait_for_run", "Poll a run until completed, failed, or cancelled (or timeout).", {
|
|
125
|
+
job_id: z.string(),
|
|
126
|
+
run_id: z.string(),
|
|
127
|
+
timeout_sec: z.number().optional(),
|
|
128
|
+
interval_sec: z.number().optional(),
|
|
129
|
+
}, async (args) => {
|
|
130
|
+
try {
|
|
131
|
+
return ok(await api.waitForRun(args.job_id, args.run_id, {
|
|
132
|
+
timeoutSec: args.timeout_sec,
|
|
133
|
+
intervalSec: args.interval_sec,
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
return fail(err);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
server.tool("list_runs", "List runs for a job (newest first).", { job_id: z.string() }, async (args) => {
|
|
141
|
+
try {
|
|
142
|
+
return ok(await api.listRuns(args.job_id));
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
return fail(err);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
server.tool("export_design", "Export a job as pdf or html (ZIP) and return a short-lived download_url. Prefer pdf for print/slides/cards; html for web landing pages. This is the standard download step — do not publish unless the user asks.", {
|
|
149
|
+
job_id: z.string(),
|
|
150
|
+
format: z.enum(["pdf", "html"]),
|
|
151
|
+
run_id: z.string().optional(),
|
|
152
|
+
}, async (args) => {
|
|
153
|
+
try {
|
|
154
|
+
return ok(await api.exportDesign(args.job_id, args.format, args.run_id));
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
return fail(err);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
server.tool("publish_design", "Publish a job to a public URL. ONLY call this when the user explicitly asks to publish or make the design public — never as a default step after create/export.", {
|
|
161
|
+
job_id: z.string(),
|
|
162
|
+
run_id: z.string().optional(),
|
|
163
|
+
}, async (args) => {
|
|
164
|
+
try {
|
|
165
|
+
return ok(await api.publishDesign(args.job_id, args.run_id));
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
return fail(err);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
server.tool("unpublish_design", "Unpublish a job's public site. Only when the user asks to take a published design down.", { job_id: z.string() }, async (args) => {
|
|
172
|
+
try {
|
|
173
|
+
return ok(await api.unpublishDesign(args.job_id));
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
return fail(err);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nexdoc/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "NexDoc Design MCP server — create, update, export, and publish designs via API key (stdio) or OAuth (HTTP).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nexdoc-mcp-server": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"prepublishOnly": "npm run build",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"dev": "tsx src/index.ts"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/nexdocai/nxd-design.git",
|
|
26
|
+
"directory": "integrations/mcp-server"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/nexdocai/nxd-design/tree/main/integrations/mcp-server",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/nexdocai/nxd-design/issues"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"mcp",
|
|
34
|
+
"model-context-protocol",
|
|
35
|
+
"nexdoc",
|
|
36
|
+
"design"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
43
|
+
"express": "^4.21.2",
|
|
44
|
+
"zod": "^3.24.2"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/express": "^4.17.21",
|
|
48
|
+
"@types/node": "^22.13.10",
|
|
49
|
+
"tsx": "^4.19.3",
|
|
50
|
+
"typescript": "^5.8.2"
|
|
51
|
+
},
|
|
52
|
+
"license": "MIT"
|
|
53
|
+
}
|