@nexdoc/mcp-server 0.1.0 → 0.2.1

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 CHANGED
@@ -28,18 +28,69 @@ Model Context Protocol server for [NexDoc Design](https://www.nexdoc.design).
28
28
 
29
29
  ## Tools
30
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`
31
+ `create_design`, `update_design`, `check_run`, `refresh_preview`, `wait_for_run`, `notify_run_email`, `list_runs`, `export_design`, `publish_design`, `unpublish_design`, `list_formats`, `get_balance`
32
+
33
+ `create_design` / `update_design` accept `notify_email: true` to email the account owner when the run finishes. `notify_run_email` does the same for a run that already started; if that run is already done, the tool returns a `warning` and no email is sent.
32
34
 
33
35
  ## Develop
34
36
 
35
37
  ```bash
38
+ cd integrations/mcp-server
36
39
  npm install
37
40
  npm run build
38
41
  NXD_API_KEY=nxd_test_... NXD_API_URL=http://127.0.0.1:8082 node dist/index.js
39
42
  ```
40
43
 
41
- Docs: [../../docs/agents/mcp.md](../../docs/agents/mcp.md)
44
+ HTTP locally:
45
+
46
+ ```bash
47
+ NXD_API_KEY=nxd_test_... NXD_API_URL=http://127.0.0.1:8082 \
48
+ node dist/index.js --http --port 8084
49
+ ```
50
+
51
+ ## Release an update (npm + hosted MCP)
52
+
53
+ `npx -y @nexdoc/mcp-server` pulls from npm. The Railway `mcp` service builds from this folder’s Dockerfile — redeploy it after merging so `https://mcp.nexdoc.design` matches the published package.
54
+
55
+ ```bash
56
+ cd integrations/mcp-server
57
+
58
+ # 1. Edit src/ (e.g. tools.ts), then bump the version
59
+ # patch 0.1.0 → 0.1.1 tool copy / bugfix
60
+ # minor 0.1.x → 0.2.0 new tool or behavior
61
+ npm version patch --no-git-tag-version # or: npm version minor --no-git-tag-version
62
+
63
+ # 2. Build (also runs automatically via prepublishOnly)
64
+ npm run build
65
+
66
+ # 3. Smoke-check the built binary
67
+ node dist/index.js --help 2>/dev/null || true
68
+ grep -n "list_runs\|oldest first" dist/tools.js # confirm your change landed
69
+
70
+ # 4. Publish — run this in your own interactive terminal (not a headless agent).
71
+ # Passkey / WebAuthn accounts cannot use --otp. npm opens a browser (or prints
72
+ # an auth URL); approve with Touch ID / Face ID / security key, then publish finishes.
73
+ npm whoami
74
+ npm publish --access public
75
+
76
+ # 5. Verify
77
+ npm view @nexdoc/mcp-server version
78
+ npx -y @nexdoc/mcp-server@latest --help 2>/dev/null || true
79
+
80
+ # 6. Redeploy the hosted HTTP service so OAuth clients get the same tools
81
+ # (Railway project → mcp service → Deploy, or your usual compose/redeploy path)
82
+ ```
42
83
 
43
- ## TODO
84
+ ### Passkey publish notes
44
85
 
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`.
86
+ - **Do the publish step yourself** in Terminal / iTerm. Agents and non-TTY shells get `EOTP` and cannot complete WebAuthn.
87
+ - If the CLI only prints an auth URL (no browser), open that URL, complete the passkey prompt, wait for success, then re-run `npm publish --access public` in the same shell if needed.
88
+ - Prefer npm CLI **≥ 11** (`npm -v`). Older CLIs expect TOTP `--otp` and fail on passkey-only accounts.
89
+ - For CI later: create a **granular access token** (or Trusted Publisher) on [npmjs.com](https://www.npmjs.com) while signed in with your passkey; do not put classic forever-tokens in git.
90
+
91
+ After publish, clients that pin no version (`npx -y @nexdoc/mcp-server`) pick up the new release on the next cold start. Restart Cursor / Claude Desktop if a long-lived stdio process is still on the old build.
92
+
93
+
94
+ Install: `npx -y @nexdoc/mcp-server`
95
+
96
+ Docs: [../../docs/agents/mcp.md](../../docs/agents/mcp.md)
package/dist/client.d.ts CHANGED
@@ -12,6 +12,7 @@ export type RunCreated = {
12
12
  run_id: string;
13
13
  status: string;
14
14
  created_at: string;
15
+ notify_email?: boolean;
15
16
  };
16
17
  export type RunStatus = {
17
18
  run_id: string;
@@ -37,6 +38,7 @@ export declare function createRunJson(jobId: string, body: {
37
38
  file_id: string;
38
39
  }>;
39
40
  webhook_url?: string;
41
+ notify_email?: boolean;
40
42
  }): Promise<RunCreated>;
41
43
  export declare function createRunMultipart(jobId: string, opts: {
42
44
  format?: string;
@@ -44,6 +46,7 @@ export declare function createRunMultipart(jobId: string, opts: {
44
46
  content?: string;
45
47
  contentPath?: string;
46
48
  assetPaths?: string[];
49
+ notify_email?: boolean;
47
50
  }): Promise<RunCreated>;
48
51
  export declare function getRun(jobId: string, runId: string): Promise<RunStatus>;
49
52
  export declare function listRuns(jobId: string): Promise<{
@@ -54,6 +57,7 @@ export declare function exportDesign(jobId: string, format: "pdf" | "html", runI
54
57
  export declare function publishDesign(jobId: string, runId?: string): Promise<Record<string, unknown>>;
55
58
  export declare function unpublishDesign(jobId: string): Promise<Record<string, unknown>>;
56
59
  export declare function getPreviewLink(jobId: string): Promise<Record<string, unknown>>;
60
+ export declare function requestRunEmail(jobId: string, runId: string): Promise<Record<string, unknown>>;
57
61
  export declare function getBalance(): Promise<Record<string, unknown>>;
58
62
  export declare function waitForRun(jobId: string, runId: string, opts?: {
59
63
  timeoutSec?: number;
package/dist/client.js CHANGED
@@ -79,6 +79,8 @@ export async function createRunMultipart(jobId, opts) {
79
79
  : "application/octet-stream";
80
80
  form.append("files", new Blob([buf], { type }), name);
81
81
  }
82
+ if (opts.notify_email)
83
+ form.set("notify_email", "true");
82
84
  return request("POST", `/v1/jobs/${jobId}/runs`, { form });
83
85
  }
84
86
  export async function getRun(jobId, runId) {
@@ -103,6 +105,9 @@ export async function unpublishDesign(jobId) {
103
105
  export async function getPreviewLink(jobId) {
104
106
  return request("GET", `/v1/jobs/${jobId}/viewer/session`);
105
107
  }
108
+ export async function requestRunEmail(jobId, runId) {
109
+ return request("POST", `/v1/jobs/${jobId}/runs/${runId}/notify-email`, { json: {} });
110
+ }
106
111
  export async function getBalance() {
107
112
  return request("GET", "/v1/credits");
108
113
  }
package/dist/tools.js CHANGED
@@ -43,6 +43,10 @@ export function registerTools(server) {
43
43
  .optional()
44
44
  .describe("If true, poll until terminal status before returning"),
45
45
  timeout_sec: z.number().optional().describe("Wait timeout seconds (default 900)"),
46
+ notify_email: z
47
+ .boolean()
48
+ .optional()
49
+ .describe("If true, email the account owner when the run finishes (completed, failed, or cancelled). Use when you cannot poll for the full duration."),
46
50
  }, async (args) => {
47
51
  try {
48
52
  let jobId = args.job_id;
@@ -58,11 +62,13 @@ export function registerTools(server) {
58
62
  content: args.content,
59
63
  contentPath: args.content_path,
60
64
  assetPaths: args.asset_paths,
65
+ notify_email: args.notify_email,
61
66
  })
62
67
  : await api.createRunJson(jobId, {
63
68
  format: args.format,
64
69
  instructions: args.instructions,
65
70
  content: args.content ?? "",
71
+ notify_email: args.notify_email,
66
72
  });
67
73
  if (args.wait) {
68
74
  const final = await api.waitForRun(jobId, created.run_id, {
@@ -70,7 +76,13 @@ export function registerTools(server) {
70
76
  });
71
77
  return ok({ job_id: jobId, run: final });
72
78
  }
73
- return ok({ job_id: jobId, run_id: created.run_id, status: created.status, created_at: created.created_at });
79
+ return ok({
80
+ job_id: jobId,
81
+ run_id: created.run_id,
82
+ status: created.status,
83
+ created_at: created.created_at,
84
+ notify_email: created.notify_email ?? false,
85
+ });
74
86
  }
75
87
  catch (err) {
76
88
  return fail(err);
@@ -83,12 +95,17 @@ export function registerTools(server) {
83
95
  content: z.string().optional(),
84
96
  wait: z.boolean().optional(),
85
97
  timeout_sec: z.number().optional(),
98
+ notify_email: z
99
+ .boolean()
100
+ .optional()
101
+ .describe("If true, email the account owner when the run finishes. Use when you cannot poll for the full duration."),
86
102
  }, async (args) => {
87
103
  try {
88
104
  const created = await api.createRunJson(args.job_id, {
89
105
  format: args.format,
90
106
  instructions: args.instructions,
91
107
  content: args.content ?? "",
108
+ notify_email: args.notify_email,
92
109
  });
93
110
  if (args.wait) {
94
111
  const final = await api.waitForRun(args.job_id, created.run_id, {
@@ -121,6 +138,17 @@ export function registerTools(server) {
121
138
  return fail(err);
122
139
  }
123
140
  });
141
+ server.tool("notify_run_email", "Request an email when an in-progress run finishes (completed, failed, or cancelled). Emails the authenticated account. If the run is already terminal, returns a warning and does not send.", {
142
+ job_id: z.string(),
143
+ run_id: z.string(),
144
+ }, async (args) => {
145
+ try {
146
+ return ok(await api.requestRunEmail(args.job_id, args.run_id));
147
+ }
148
+ catch (err) {
149
+ return fail(err);
150
+ }
151
+ });
124
152
  server.tool("wait_for_run", "Poll a run until completed, failed, or cancelled (or timeout).", {
125
153
  job_id: z.string(),
126
154
  run_id: z.string(),
@@ -137,7 +165,7 @@ export function registerTools(server) {
137
165
  return fail(err);
138
166
  }
139
167
  });
140
- server.tool("list_runs", "List runs for a job (newest first).", { job_id: z.string() }, async (args) => {
168
+ server.tool("list_runs", "List runs for a job (oldest first; the last entry is the latest run).", { job_id: z.string() }, async (args) => {
141
169
  try {
142
170
  return ok(await api.listRuns(args.job_id));
143
171
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexdoc/mcp-server",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "NexDoc Design MCP server — create, update, export, and publish designs via API key (stdio) or OAuth (HTTP).",
5
5
  "type": "module",
6
6
  "bin": {