@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 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`.
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ export type RequestAuth = {
3
+ bearer: string;
4
+ };
5
+ export declare const authStore: AsyncLocalStorage<RequestAuth>;
6
+ export declare function getBearer(): string;
7
+ export declare function getApiBase(): string;
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ export type FormatInfo = {
2
+ slug: string;
3
+ family: string;
4
+ when: string;
5
+ };
6
+ export declare const FORMATS: FormatInfo[];
@@ -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
+ ];
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
+ });
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function createServer(): McpServer;
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
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerTools(server: McpServer): void;
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
+ }