@leafwright/mcp 0.2.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 +179 -0
- package/leafwright-tools.md +70 -0
- package/package.json +49 -0
- package/server.mjs +1099 -0
package/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# @leafwright/mcp
|
|
2
|
+
|
|
3
|
+
The official [Leafwright](https://leafwright.co) MCP server. It gives an AI
|
|
4
|
+
coding agent (Claude Code, Cursor, Codex, and any MCP client) the ability to
|
|
5
|
+
**generate PDFs, build templates, and debug failed renders from their logs** —
|
|
6
|
+
all through the Leafwright public API.
|
|
7
|
+
|
|
8
|
+
No headless Chrome, no Puppeteer in your bundle. The agent calls a tool, gets a
|
|
9
|
+
real signed PDF URL back.
|
|
10
|
+
|
|
11
|
+
## Connect from claude.ai (remote, no install)
|
|
12
|
+
|
|
13
|
+
Leafwright also runs as a remote MCP server with OAuth. In claude.ai →
|
|
14
|
+
Settings → Connectors → **Add custom connector**, enter:
|
|
15
|
+
|
|
16
|
+
- **Name**: Leafwright
|
|
17
|
+
- **Remote MCP server URL**: `https://leafwright.co/mcp`
|
|
18
|
+
|
|
19
|
+
Leave the OAuth fields empty — the connector registers itself and opens a
|
|
20
|
+
browser window where you sign in and pick an organization and default project.
|
|
21
|
+
Works with any MCP client that supports OAuth 2.1 + PKCE and Dynamic Client
|
|
22
|
+
Registration.
|
|
23
|
+
|
|
24
|
+
## Install (local stdio)
|
|
25
|
+
|
|
26
|
+
Nothing to install — run it with `npx`:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npx -y @leafwright/mcp
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Start the server without credentials, then call `leafwright_login` from your
|
|
33
|
+
agent. Open the returned verification URL, sign in, choose an organization and
|
|
34
|
+
default project, then call `leafwright_auth_status`.
|
|
35
|
+
|
|
36
|
+
Project API keys are still supported for server automation:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
LEAFWRIGHT_API_KEY=lw_live_...
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Get an account at [leafwright.co](https://leafwright.co).
|
|
43
|
+
|
|
44
|
+
## Configure your client
|
|
45
|
+
|
|
46
|
+
**Claude Code** (`~/.claude/mcp.json` or project `.mcp.json`), **Cursor**
|
|
47
|
+
(`~/.cursor/mcp.json`), and **Codex** all use the same shape:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"mcpServers": {
|
|
52
|
+
"leafwright": {
|
|
53
|
+
"command": "npx",
|
|
54
|
+
"args": ["-y", "@leafwright/mcp"],
|
|
55
|
+
"env": {}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Restart the client. Before login, only auth/help tools appear; after browser
|
|
62
|
+
auth, the full `leafwright_*` tool set appears.
|
|
63
|
+
|
|
64
|
+
## Environment
|
|
65
|
+
|
|
66
|
+
| Variable | Required | Default | Purpose |
|
|
67
|
+
| --- | --- | --- | --- |
|
|
68
|
+
| `LEAFWRIGHT_API_KEY` | no | — | Backward-compatible project API key (`lw_live_…` or `lw_test_…`). |
|
|
69
|
+
| `LEAFWRIGHT_MCP_TOKEN` | no | — | MCP token (`mcp_…`) for non-interactive setups. |
|
|
70
|
+
| `LEAFWRIGHT_PROJECT_ID` | no | selected project | Override the active default project. |
|
|
71
|
+
| `LEAFWRIGHT_MCP_READ_ONLY` | no | `false` | `true` exposes only read tools — no renders, no template writes, no AI. |
|
|
72
|
+
|
|
73
|
+
## Tools
|
|
74
|
+
|
|
75
|
+
**Auth and context:**
|
|
76
|
+
|
|
77
|
+
| Tool | What it does |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `leafwright_login` | Start browser login. |
|
|
80
|
+
| `leafwright_auth_status` | Complete/check browser login. |
|
|
81
|
+
| `leafwright_logout` | Revoke and clear local MCP auth. |
|
|
82
|
+
| `leafwright_list_organizations` | List the selected token's organization. |
|
|
83
|
+
| `leafwright_list_projects` | List projects in the selected organization. |
|
|
84
|
+
| `leafwright_select_project` | Set the local active project. |
|
|
85
|
+
|
|
86
|
+
**Read (available after auth, no side effects):**
|
|
87
|
+
|
|
88
|
+
| Tool | What it does |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| `leafwright_list_templates` | List the project's templates. |
|
|
91
|
+
| `leafwright_get_template` | Fetch one template (draft HTML/CSS, sample data, versions). |
|
|
92
|
+
| `leafwright_validate_template` | Validate a template / draft overrides before rendering. |
|
|
93
|
+
| `leafwright_get_job` | Render job status and metadata. |
|
|
94
|
+
| `leafwright_get_job_logs` | Render logs — failed assets, fonts, timeouts, overflow. |
|
|
95
|
+
| `leafwright_get_usage` | Credits used/remaining for the billing period. |
|
|
96
|
+
| `leafwright_estimate_usage_cost` | Estimate credits and overage cost for a planned batch of renders. |
|
|
97
|
+
|
|
98
|
+
**Write (hidden when `LEAFWRIGHT_MCP_READ_ONLY=true`):**
|
|
99
|
+
|
|
100
|
+
| Tool | What it does |
|
|
101
|
+
| --- | --- |
|
|
102
|
+
| `leafwright_render_html` | Render raw HTML → PDF (signed URL). |
|
|
103
|
+
| `leafwright_render_url` | Render a public URL → PDF (SSRF-protected). |
|
|
104
|
+
| `leafwright_render_template` | Render a published template with JSON data. |
|
|
105
|
+
| `leafwright_create_template` | Create a draft template. |
|
|
106
|
+
| `leafwright_update_template_draft` | Update a draft's HTML/CSS/name/sample data. |
|
|
107
|
+
| `leafwright_ai_generate_template` | Generate a draft template from a prompt. |
|
|
108
|
+
| `leafwright_ai_revise_template` | Revise a draft from a natural-language instruction. |
|
|
109
|
+
| `leafwright_ai_fix_from_logs` | **Repair a template from a failed render's logs.** |
|
|
110
|
+
|
|
111
|
+
## HTML rules
|
|
112
|
+
|
|
113
|
+
Leafwright validates all HTML before rendering — the same rules apply to
|
|
114
|
+
`leafwright_render_html`, template create/update, and validation.
|
|
115
|
+
|
|
116
|
+
**Supported**
|
|
117
|
+
|
|
118
|
+
- Full documents: doctype, `<html>`, `<head>`, `<meta>`, `<title>`, `<body>`,
|
|
119
|
+
and `<style>` blocks.
|
|
120
|
+
- Text, heading, list, table, and layout tags (`div`, `section`, `header`,
|
|
121
|
+
`footer`, `main`, `article`, `figure`).
|
|
122
|
+
- `<img>` with base64 data URIs (`data:image/png`, `jpeg`, `gif`, `webp`, or
|
|
123
|
+
`svg+xml`), relative paths, or `cid:` references.
|
|
124
|
+
- `class`, `style`, `id`, standard layout/table/meta/image attributes, and
|
|
125
|
+
`aria-*` / `data-*`.
|
|
126
|
+
- Handlebars placeholders like `{{customer.name}}` in templates.
|
|
127
|
+
|
|
128
|
+
**Rejected**
|
|
129
|
+
|
|
130
|
+
- `<script>`, `<iframe>`, `<object>`, `<embed>`, `<form>`, `<link>`, and inline
|
|
131
|
+
event handlers.
|
|
132
|
+
- Inline `<svg>` markup (`svg`, `path`, `rect`, `g`, and the rest of the SVG
|
|
133
|
+
element family). Encode the SVG as a **base64** data URI and reference it
|
|
134
|
+
from an `<img>` instead — URL-encoded SVG data URIs are rejected:
|
|
135
|
+
|
|
136
|
+
```html
|
|
137
|
+
<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0i..." alt="Logo">
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- SVG in CSS `url()` backgrounds — use an `<img>` element instead.
|
|
141
|
+
- External http(s) URLs in `src` attributes or CSS `url()` — the renderer never
|
|
142
|
+
fetches remote assets for HTML sources. Embed images as data URIs instead.
|
|
143
|
+
- CSS `@import`.
|
|
144
|
+
|
|
145
|
+
Size limits: HTML 250 KB, CSS 120 KB, up to 200 pages per PDF. To render a
|
|
146
|
+
public web page *with* its remote assets, use `leafwright_render_url`, which
|
|
147
|
+
fetches the page behind SSRF protections.
|
|
148
|
+
|
|
149
|
+
## Prompts
|
|
150
|
+
|
|
151
|
+
Ready-made workflows your client can invoke: `create_invoice_template`,
|
|
152
|
+
`create_report_template`, `create_template_from_prompt`, `debug_pdf_layout`,
|
|
153
|
+
`explain_render_failure`, `integrate_nextjs`, `migrate_from_puppeteer`.
|
|
154
|
+
|
|
155
|
+
## Resources
|
|
156
|
+
|
|
157
|
+
`leafwright://docs` (API quickstart), `leafwright://templates` (your
|
|
158
|
+
templates), `leafwright://usage` (current usage), `leafwright://jobs/recent`
|
|
159
|
+
(the 20 most recent render jobs), `leafwright://jobs/{jobId}/logs` (logs for a
|
|
160
|
+
specific job).
|
|
161
|
+
|
|
162
|
+
## The debugging loop
|
|
163
|
+
|
|
164
|
+
A code-only agent can't see its PDF. This server closes that loop:
|
|
165
|
+
|
|
166
|
+
1. Agent renders a template → it fails or looks wrong.
|
|
167
|
+
2. Agent calls `leafwright_get_job_logs` → reads the real cause.
|
|
168
|
+
3. Agent calls `leafwright_ai_fix_from_logs` → gets a fix, re-renders, confirms.
|
|
169
|
+
|
|
170
|
+
## Safety
|
|
171
|
+
|
|
172
|
+
The server only calls the Leafwright public API. It runs no shell commands from
|
|
173
|
+
agent input, stores MCP tokens in the OS keychain when possible, falls back to a
|
|
174
|
+
permission-restricted config file, and supports read-only mode. AI **drafts**
|
|
175
|
+
templates; publishing always requires a human in the dashboard.
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT · [leafwright.co](https://leafwright.co)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Leafwright MCP Tools
|
|
2
|
+
|
|
3
|
+
The runnable stdio MCP server lives at `mcp/server.mjs` and ships as
|
|
4
|
+
`@leafwright/mcp`. See `README.md` for setup. Full reference below.
|
|
5
|
+
|
|
6
|
+
## Auth and context tools
|
|
7
|
+
|
|
8
|
+
- `leafwright_login`: start a browser login and return a verification URL.
|
|
9
|
+
- `leafwright_auth_status`: check or complete browser login.
|
|
10
|
+
- `leafwright_logout`: revoke and clear local MCP auth.
|
|
11
|
+
- `leafwright_list_organizations`: list the token's selected organization.
|
|
12
|
+
- `leafwright_list_projects`: list available projects in that organization.
|
|
13
|
+
- `leafwright_select_project`: set the local active project.
|
|
14
|
+
|
|
15
|
+
## Read tools (available after auth)
|
|
16
|
+
|
|
17
|
+
- `leafwright_list_templates`: list project templates.
|
|
18
|
+
- `leafwright_get_template`: fetch one template (draft HTML/CSS, sample data, versions).
|
|
19
|
+
- `leafwright_validate_template`: validate a template or draft override.
|
|
20
|
+
- `leafwright_get_job`: fetch render job status and metadata.
|
|
21
|
+
- `leafwright_get_job_logs`: fetch render logs for debugging.
|
|
22
|
+
- `leafwright_get_usage`: credits used/remaining for the billing period.
|
|
23
|
+
- `leafwright_estimate_usage_cost`: estimate credits and overage cost for a
|
|
24
|
+
planned batch of renders.
|
|
25
|
+
|
|
26
|
+
## Write tools (hidden when `LEAFWRIGHT_MCP_READ_ONLY=true`)
|
|
27
|
+
|
|
28
|
+
- `leafwright_render_html`: render raw HTML to PDF.
|
|
29
|
+
- `leafwright_render_url`: render a public URL to PDF after SSRF checks.
|
|
30
|
+
- `leafwright_render_template`: render a published template with JSON data.
|
|
31
|
+
- `leafwright_create_template`: create a draft template.
|
|
32
|
+
- `leafwright_update_template_draft`: update a draft's HTML/CSS/name/sample data.
|
|
33
|
+
- `leafwright_ai_generate_template`: create an AI-assisted draft template.
|
|
34
|
+
- `leafwright_ai_revise_template`: revise a draft from an instruction (preview or apply).
|
|
35
|
+
- `leafwright_ai_fix_from_logs`: repair a draft from a failed render's logs.
|
|
36
|
+
|
|
37
|
+
## HTML rules
|
|
38
|
+
|
|
39
|
+
The same validation applies to `leafwright_render_html`, template
|
|
40
|
+
create/update, and validation. Full documents are supported (doctype, `html`,
|
|
41
|
+
`head`, `meta`, `title`, `body`, `style`) along with text, table, and layout
|
|
42
|
+
tags. Images must be base64 data URIs (`png`, `jpeg`, `gif`, `webp`, or
|
|
43
|
+
`svg+xml`), relative paths, or `cid:` references. Rejected: `<script>`,
|
|
44
|
+
`<iframe>`, `<object>`, `<embed>`, `<form>`, `<link>`, inline `<svg>` markup
|
|
45
|
+
(base64-encode the SVG into an `<img>` data URI instead — URL-encoded SVG
|
|
46
|
+
data URIs are also rejected), SVG in CSS `url()`, inline event handlers,
|
|
47
|
+
external http(s) asset URLs, and CSS `@import`/external `url()`. Limits:
|
|
48
|
+
HTML 250 KB, CSS 120 KB, 200 pages per PDF. The `leafwright://docs` resource
|
|
49
|
+
carries the full list.
|
|
50
|
+
|
|
51
|
+
## Prompts
|
|
52
|
+
|
|
53
|
+
`create_invoice_template`, `create_report_template`, `create_template_from_prompt`,
|
|
54
|
+
`debug_pdf_layout`, `explain_render_failure`, `integrate_nextjs`,
|
|
55
|
+
`migrate_from_puppeteer`.
|
|
56
|
+
|
|
57
|
+
## Resources
|
|
58
|
+
|
|
59
|
+
`leafwright://docs`, `leafwright://templates`, `leafwright://usage`,
|
|
60
|
+
`leafwright://jobs/recent`, `leafwright://jobs/{jobId}/logs`.
|
|
61
|
+
|
|
62
|
+
## Environment
|
|
63
|
+
|
|
64
|
+
- `LEAFWRIGHT_API_KEY`: optional backward-compatible project API key.
|
|
65
|
+
- `LEAFWRIGHT_MCP_TOKEN`: optional non-interactive MCP token.
|
|
66
|
+
- `LEAFWRIGHT_PROJECT_ID`: optional active project override.
|
|
67
|
+
- `LEAFWRIGHT_MCP_READ_ONLY`: optional, `true` exposes read tools only.
|
|
68
|
+
|
|
69
|
+
Do not pass API keys or MCP tokens through tool arguments. Prefer browser auth
|
|
70
|
+
with `leafwright_login`; MCP clients inject env credentials only for automation.
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@leafwright/mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Model Context Protocol server for Leafwright — let your AI coding agent generate, template, and debug PDFs.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"leafwright-mcp": "./server.mjs"
|
|
8
|
+
},
|
|
9
|
+
"main": "server.mjs",
|
|
10
|
+
"files": [
|
|
11
|
+
"server.mjs",
|
|
12
|
+
"README.md",
|
|
13
|
+
"leafwright-tools.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "node server.mjs"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
23
|
+
"zod": "^4.3.6"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"mcp",
|
|
27
|
+
"model-context-protocol",
|
|
28
|
+
"pdf",
|
|
29
|
+
"html-to-pdf",
|
|
30
|
+
"leafwright",
|
|
31
|
+
"claude",
|
|
32
|
+
"cursor",
|
|
33
|
+
"ai-agent",
|
|
34
|
+
"puppeteer-alternative"
|
|
35
|
+
],
|
|
36
|
+
"homepage": "https://leafwright.co",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/Tactic-Systems/leafwright.git",
|
|
40
|
+
"directory": "mcp"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/Tactic-Systems/leafwright/issues"
|
|
44
|
+
},
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/server.mjs
ADDED
|
@@ -0,0 +1,1099 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
import * as z from "zod/v4";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_BASE_URL = "https://leafwright.co";
|
|
14
|
+
const KEYCHAIN_SERVICE = "leafwright-mcp";
|
|
15
|
+
const KEYCHAIN_ACCOUNT = "default";
|
|
16
|
+
const CONFIG_PATH = join(homedir(), ".config", "leafwright", "mcp-auth.json");
|
|
17
|
+
const runtimeStorage = new AsyncLocalStorage();
|
|
18
|
+
let runtimeOverrides = {};
|
|
19
|
+
|
|
20
|
+
function currentRuntimeOverrides() {
|
|
21
|
+
return runtimeStorage.getStore() ?? runtimeOverrides;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function runWithLeafwrightMcpRuntime(options, callback) {
|
|
25
|
+
return runtimeStorage.run(options ?? {}, callback);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readJsonFile(path) {
|
|
29
|
+
try {
|
|
30
|
+
if (!existsSync(path)) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function writeJsonFile(path, value) {
|
|
41
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
42
|
+
writeFileSync(path, JSON.stringify(value, null, 2), { mode: 0o600 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readKeychainToken() {
|
|
46
|
+
if (process.platform !== "darwin") {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
return execFileSync("security", [
|
|
52
|
+
"find-generic-password",
|
|
53
|
+
"-s",
|
|
54
|
+
KEYCHAIN_SERVICE,
|
|
55
|
+
"-a",
|
|
56
|
+
KEYCHAIN_ACCOUNT,
|
|
57
|
+
"-w"
|
|
58
|
+
], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeKeychainToken(token) {
|
|
65
|
+
if (process.platform !== "darwin") {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
execFileSync("security", [
|
|
71
|
+
"add-generic-password",
|
|
72
|
+
"-U",
|
|
73
|
+
"-s",
|
|
74
|
+
KEYCHAIN_SERVICE,
|
|
75
|
+
"-a",
|
|
76
|
+
KEYCHAIN_ACCOUNT,
|
|
77
|
+
"-w",
|
|
78
|
+
token
|
|
79
|
+
], { stdio: "ignore" });
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function deleteKeychainToken() {
|
|
87
|
+
if (process.platform !== "darwin") {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
execFileSync("security", [
|
|
93
|
+
"delete-generic-password",
|
|
94
|
+
"-s",
|
|
95
|
+
KEYCHAIN_SERVICE,
|
|
96
|
+
"-a",
|
|
97
|
+
KEYCHAIN_ACCOUNT
|
|
98
|
+
], { stdio: "ignore" });
|
|
99
|
+
} catch {
|
|
100
|
+
// Missing keychain entry is fine.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function readStoredAuth() {
|
|
105
|
+
const fileAuth = readJsonFile(CONFIG_PATH) ?? {};
|
|
106
|
+
const keychainToken = readKeychainToken();
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
...fileAuth,
|
|
110
|
+
accessToken: keychainToken || fileAuth.accessToken || null,
|
|
111
|
+
tokenStorage: keychainToken ? "keychain" : fileAuth.accessToken ? "file" : null
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function writeStoredAuth(auth) {
|
|
116
|
+
if (
|
|
117
|
+
currentRuntimeOverrides().disableLocalStorage ||
|
|
118
|
+
process.env.LEAFWRIGHT_MCP_DISABLE_LOCAL_AUTH === "true"
|
|
119
|
+
) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const stored = {
|
|
124
|
+
...readStoredAuth(),
|
|
125
|
+
...auth,
|
|
126
|
+
updatedAt: new Date().toISOString()
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (stored.accessToken && writeKeychainToken(stored.accessToken)) {
|
|
130
|
+
const { accessToken: _accessToken, ...fileValue } = stored;
|
|
131
|
+
writeJsonFile(CONFIG_PATH, fileValue);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
writeJsonFile(CONFIG_PATH, stored);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function clearStoredAuth() {
|
|
139
|
+
if (
|
|
140
|
+
currentRuntimeOverrides().disableLocalStorage ||
|
|
141
|
+
process.env.LEAFWRIGHT_MCP_DISABLE_LOCAL_AUTH === "true"
|
|
142
|
+
) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
deleteKeychainToken();
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
unlinkSync(CONFIG_PATH);
|
|
150
|
+
} catch {
|
|
151
|
+
// Missing config file is fine.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function config() {
|
|
156
|
+
const overrides = currentRuntimeOverrides();
|
|
157
|
+
const disableLocalStorage =
|
|
158
|
+
overrides.disableLocalStorage || process.env.LEAFWRIGHT_MCP_DISABLE_LOCAL_AUTH === "true";
|
|
159
|
+
const stored = disableLocalStorage ? {} : readStoredAuth();
|
|
160
|
+
const apiKey = overrides.apiKey ?? process.env.LEAFWRIGHT_API_KEY ?? null;
|
|
161
|
+
const mcpToken =
|
|
162
|
+
overrides.mcpToken ?? process.env.LEAFWRIGHT_MCP_TOKEN ?? stored.accessToken ?? null;
|
|
163
|
+
const bearerToken = overrides.bearerToken ?? apiKey ?? mcpToken;
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
apiKey,
|
|
167
|
+
mcpToken,
|
|
168
|
+
bearerToken,
|
|
169
|
+
tokenSource: overrides.tokenSource || (apiKey
|
|
170
|
+
? "api_key_env"
|
|
171
|
+
: process.env.LEAFWRIGHT_MCP_TOKEN
|
|
172
|
+
? "mcp_token_env"
|
|
173
|
+
: stored.accessToken
|
|
174
|
+
? stored.tokenStorage || "stored"
|
|
175
|
+
: bearerToken
|
|
176
|
+
? "bearer"
|
|
177
|
+
: null),
|
|
178
|
+
baseUrl: (overrides.baseUrl || process.env.LEAFWRIGHT_API_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, ""),
|
|
179
|
+
readOnly: overrides.readOnly ?? process.env.LEAFWRIGHT_MCP_READ_ONLY === "true",
|
|
180
|
+
activeProjectId:
|
|
181
|
+
overrides.activeProjectId ||
|
|
182
|
+
process.env.LEAFWRIGHT_PROJECT_ID ||
|
|
183
|
+
stored.activeProjectId ||
|
|
184
|
+
stored.token?.defaultProjectId ||
|
|
185
|
+
null
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isAuthenticated() {
|
|
190
|
+
return Boolean(config().bearerToken);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function leafwrightRequest(path, init = {}) {
|
|
194
|
+
const { bearerToken, baseUrl } = config();
|
|
195
|
+
|
|
196
|
+
if (!bearerToken) {
|
|
197
|
+
throw new Error("Leafwright MCP is not authenticated. Run leafwright_login first.");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
201
|
+
...init,
|
|
202
|
+
headers: {
|
|
203
|
+
"content-type": "application/json",
|
|
204
|
+
authorization: `Bearer ${bearerToken}`,
|
|
205
|
+
...init.headers
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
const text = await response.text();
|
|
209
|
+
const payload = text ? JSON.parse(text) : {};
|
|
210
|
+
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
const message =
|
|
213
|
+
payload?.error?.message || payload?.message || `Leafwright request failed: ${response.status}`;
|
|
214
|
+
const error = new Error(message);
|
|
215
|
+
error.status = response.status;
|
|
216
|
+
error.payload = payload;
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return payload;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function anonymousLeafwrightRequest(path, init = {}) {
|
|
224
|
+
const { baseUrl } = config();
|
|
225
|
+
const response = await fetch(`${baseUrl}${path}`, {
|
|
226
|
+
...init,
|
|
227
|
+
headers: {
|
|
228
|
+
"content-type": "application/json",
|
|
229
|
+
...init.headers
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
const text = await response.text();
|
|
233
|
+
const payload = text ? JSON.parse(text) : {};
|
|
234
|
+
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
const message =
|
|
237
|
+
payload?.error?.message || payload?.message || `Leafwright request failed: ${response.status}`;
|
|
238
|
+
const error = new Error(message);
|
|
239
|
+
error.status = response.status;
|
|
240
|
+
error.payload = payload;
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return payload;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function withProjectBody(body = {}, explicitProjectId) {
|
|
248
|
+
const projectId = explicitProjectId || config().activeProjectId;
|
|
249
|
+
|
|
250
|
+
return projectId ? { ...body, project_id: projectId } : body;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function withProjectQuery(path, explicitProjectId) {
|
|
254
|
+
const projectId = explicitProjectId || config().activeProjectId;
|
|
255
|
+
|
|
256
|
+
if (!projectId) {
|
|
257
|
+
return path;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const separator = path.includes("?") ? "&" : "?";
|
|
261
|
+
return `${path}${separator}project_id=${encodeURIComponent(projectId)}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function toolResult(payload) {
|
|
265
|
+
return {
|
|
266
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
267
|
+
structuredContent: payload
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function toolError(error) {
|
|
272
|
+
const payload = {
|
|
273
|
+
error: {
|
|
274
|
+
message: error instanceof Error ? error.message : "Leafwright MCP tool failed.",
|
|
275
|
+
status: typeof error?.status === "number" ? error.status : undefined,
|
|
276
|
+
details: error?.payload?.error?.details ?? error?.payload?.error ?? undefined
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
isError: true,
|
|
282
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
|
|
283
|
+
structuredContent: payload
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function tool(run) {
|
|
288
|
+
return async (args) => {
|
|
289
|
+
try {
|
|
290
|
+
return toolResult(await run(args ?? {}));
|
|
291
|
+
} catch (error) {
|
|
292
|
+
return toolError(error);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function registerAuthTools(server, ensureFullTools) {
|
|
298
|
+
server.registerTool(
|
|
299
|
+
"leafwright_login",
|
|
300
|
+
{
|
|
301
|
+
title: "Log in to Leafwright",
|
|
302
|
+
description:
|
|
303
|
+
"Start a browser login flow for Leafwright MCP. Open the returned URL, choose an organization and project, then call leafwright_auth_status.",
|
|
304
|
+
inputSchema: {
|
|
305
|
+
client_name: z.string().optional().describe("Optional client name shown in Leafwright.")
|
|
306
|
+
},
|
|
307
|
+
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
308
|
+
},
|
|
309
|
+
tool(async ({ client_name }) => {
|
|
310
|
+
const started = await anonymousLeafwrightRequest("/api/v1/mcp/auth/start", {
|
|
311
|
+
method: "POST",
|
|
312
|
+
body: JSON.stringify({ client_name: client_name || "Leafwright MCP" })
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
writeStoredAuth({
|
|
316
|
+
pendingDeviceCode: started.device_code,
|
|
317
|
+
pendingUserCode: started.user_code,
|
|
318
|
+
pendingExpiresAt: started.expires_at,
|
|
319
|
+
baseUrl: config().baseUrl
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
status: "pending",
|
|
324
|
+
verification_url: started.verification_url,
|
|
325
|
+
user_code: started.user_code,
|
|
326
|
+
expires_at: started.expires_at,
|
|
327
|
+
next_step:
|
|
328
|
+
"Open verification_url, approve access, then call leafwright_auth_status."
|
|
329
|
+
};
|
|
330
|
+
})
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
server.registerTool(
|
|
334
|
+
"leafwright_auth_status",
|
|
335
|
+
{
|
|
336
|
+
title: "Leafwright auth status",
|
|
337
|
+
description: "Check whether Leafwright MCP is authenticated. Completes a pending browser login if approved.",
|
|
338
|
+
inputSchema: {},
|
|
339
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
340
|
+
},
|
|
341
|
+
tool(async () => {
|
|
342
|
+
const current = config();
|
|
343
|
+
|
|
344
|
+
if (current.bearerToken) {
|
|
345
|
+
ensureFullTools();
|
|
346
|
+
return {
|
|
347
|
+
status: "connected",
|
|
348
|
+
token_source: current.tokenSource,
|
|
349
|
+
active_project_id: current.activeProjectId
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const stored =
|
|
354
|
+
currentRuntimeOverrides().disableLocalStorage ||
|
|
355
|
+
process.env.LEAFWRIGHT_MCP_DISABLE_LOCAL_AUTH === "true"
|
|
356
|
+
? {}
|
|
357
|
+
: readStoredAuth();
|
|
358
|
+
|
|
359
|
+
if (!stored.pendingDeviceCode) {
|
|
360
|
+
return {
|
|
361
|
+
status: "not_authenticated",
|
|
362
|
+
next_step: "Call leafwright_login to start browser authentication."
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const tokenResponse = await anonymousLeafwrightRequest("/api/v1/mcp/auth/token", {
|
|
367
|
+
method: "POST",
|
|
368
|
+
body: JSON.stringify({ device_code: stored.pendingDeviceCode })
|
|
369
|
+
}).catch((error) => {
|
|
370
|
+
if (error.status === 202) {
|
|
371
|
+
return error.payload;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
throw error;
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
if (tokenResponse.status === "pending") {
|
|
378
|
+
return {
|
|
379
|
+
status: "pending",
|
|
380
|
+
user_code: stored.pendingUserCode,
|
|
381
|
+
expires_at: tokenResponse.expires_at || stored.pendingExpiresAt
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
writeStoredAuth({
|
|
386
|
+
accessToken: tokenResponse.access_token,
|
|
387
|
+
token: tokenResponse.token,
|
|
388
|
+
activeProjectId: tokenResponse.token?.defaultProjectId,
|
|
389
|
+
pendingDeviceCode: null,
|
|
390
|
+
pendingUserCode: null,
|
|
391
|
+
pendingExpiresAt: null
|
|
392
|
+
});
|
|
393
|
+
ensureFullTools();
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
status: "connected",
|
|
397
|
+
token: tokenResponse.token,
|
|
398
|
+
active_project_id: tokenResponse.token?.defaultProjectId
|
|
399
|
+
};
|
|
400
|
+
})
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
server.registerTool(
|
|
404
|
+
"leafwright_logout",
|
|
405
|
+
{
|
|
406
|
+
title: "Log out of Leafwright",
|
|
407
|
+
description: "Revoke the stored Leafwright MCP token and clear local auth state.",
|
|
408
|
+
inputSchema: {},
|
|
409
|
+
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
410
|
+
},
|
|
411
|
+
tool(async () => {
|
|
412
|
+
const { bearerToken, mcpToken } = config();
|
|
413
|
+
|
|
414
|
+
if (mcpToken || bearerToken?.startsWith("mcp_")) {
|
|
415
|
+
await leafwrightRequest("/api/v1/mcp/auth/revoke", { method: "POST" }).catch(() => null);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
clearStoredAuth();
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
status: "logged_out"
|
|
422
|
+
};
|
|
423
|
+
})
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function registerContextTools(server) {
|
|
428
|
+
server.registerTool(
|
|
429
|
+
"leafwright_list_organizations",
|
|
430
|
+
{
|
|
431
|
+
title: "List Leafwright organizations",
|
|
432
|
+
description: "List organizations available to the authenticated Leafwright MCP token.",
|
|
433
|
+
inputSchema: {},
|
|
434
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
435
|
+
},
|
|
436
|
+
tool(() => leafwrightRequest("/api/v1/mcp/context", { method: "GET" }))
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
server.registerTool(
|
|
440
|
+
"leafwright_list_projects",
|
|
441
|
+
{
|
|
442
|
+
title: "List Leafwright projects",
|
|
443
|
+
description: "List projects available to the authenticated Leafwright MCP token.",
|
|
444
|
+
inputSchema: {},
|
|
445
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
446
|
+
},
|
|
447
|
+
tool(async () => {
|
|
448
|
+
const context = await leafwrightRequest("/api/v1/mcp/context", { method: "GET" });
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
data: context.projects ?? context.data?.projects ?? []
|
|
452
|
+
};
|
|
453
|
+
})
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
server.registerTool(
|
|
457
|
+
"leafwright_select_project",
|
|
458
|
+
{
|
|
459
|
+
title: "Select active Leafwright project",
|
|
460
|
+
description: "Set the local default project for future project-scoped MCP tool calls.",
|
|
461
|
+
inputSchema: {
|
|
462
|
+
project_id: z.string().min(1).describe("Project ID to use as the local active project.")
|
|
463
|
+
},
|
|
464
|
+
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
465
|
+
},
|
|
466
|
+
tool(async ({ project_id }) => {
|
|
467
|
+
const context = await leafwrightRequest("/api/v1/mcp/context", { method: "GET" });
|
|
468
|
+
const projects = context.projects ?? context.data?.projects ?? [];
|
|
469
|
+
const project = projects.find((candidate) => candidate.id === project_id);
|
|
470
|
+
|
|
471
|
+
if (!project) {
|
|
472
|
+
throw new Error("Project is not available to this MCP token.");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
writeStoredAuth({ activeProjectId: project.id });
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
active_project_id: project.id,
|
|
479
|
+
project
|
|
480
|
+
};
|
|
481
|
+
})
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function registerReadTools(server) {
|
|
486
|
+
server.registerTool(
|
|
487
|
+
"leafwright_list_templates",
|
|
488
|
+
{
|
|
489
|
+
title: "List templates",
|
|
490
|
+
description: "List templates available to the active Leafwright project.",
|
|
491
|
+
inputSchema: { project_id: z.string().optional().describe("Optional project override.") },
|
|
492
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
493
|
+
},
|
|
494
|
+
tool(({ project_id }) => leafwrightRequest(withProjectQuery("/api/v1/templates", project_id), { method: "GET" }))
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
server.registerTool(
|
|
498
|
+
"leafwright_get_template",
|
|
499
|
+
{
|
|
500
|
+
title: "Get template",
|
|
501
|
+
description: "Fetch a single template, including its draft HTML/CSS, sample data, and published versions.",
|
|
502
|
+
inputSchema: { template_id: z.string().min(1).describe("Template ID.") },
|
|
503
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
504
|
+
},
|
|
505
|
+
tool(({ template_id }) =>
|
|
506
|
+
leafwrightRequest(`/api/v1/templates/${encodeURIComponent(template_id)}`, { method: "GET" })
|
|
507
|
+
)
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
server.registerTool(
|
|
511
|
+
"leafwright_validate_template",
|
|
512
|
+
{
|
|
513
|
+
title: "Validate template",
|
|
514
|
+
description:
|
|
515
|
+
"Validate a template (optionally with draft HTML/CSS/sample-data overrides) before rendering. Validation errors state which tag, attribute, or URL was rejected and what to use instead; the leafwright://docs resource lists the full HTML rules.",
|
|
516
|
+
inputSchema: {
|
|
517
|
+
template_id: z.string().min(1).describe("Template ID."),
|
|
518
|
+
html: z.string().optional().describe("Optional draft HTML override."),
|
|
519
|
+
css: z.string().optional().describe("Optional draft CSS override."),
|
|
520
|
+
sample_data: z.record(z.string(), z.unknown()).optional().describe("Optional sample data override.")
|
|
521
|
+
},
|
|
522
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
523
|
+
},
|
|
524
|
+
tool(({ template_id, html, css, sample_data }) =>
|
|
525
|
+
leafwrightRequest(`/api/v1/templates/${encodeURIComponent(template_id)}/validate`, {
|
|
526
|
+
method: "POST",
|
|
527
|
+
body: JSON.stringify({ html, css, sample_data })
|
|
528
|
+
})
|
|
529
|
+
)
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
server.registerTool(
|
|
533
|
+
"leafwright_get_job",
|
|
534
|
+
{
|
|
535
|
+
title: "Get render job",
|
|
536
|
+
description: "Fetch render job status and metadata (pages, credits used, file size, duration).",
|
|
537
|
+
inputSchema: { job_id: z.string().min(1).describe("Render job ID.") },
|
|
538
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
539
|
+
},
|
|
540
|
+
tool(({ job_id }) =>
|
|
541
|
+
leafwrightRequest(`/api/v1/jobs/${encodeURIComponent(job_id)}`, { method: "GET" })
|
|
542
|
+
)
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
server.registerTool(
|
|
546
|
+
"leafwright_get_job_logs",
|
|
547
|
+
{
|
|
548
|
+
title: "Get render job logs",
|
|
549
|
+
description:
|
|
550
|
+
"Fetch render logs for a job. Use this to debug a failed or wrong-looking render.",
|
|
551
|
+
inputSchema: { job_id: z.string().min(1).describe("Render job ID.") },
|
|
552
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
553
|
+
},
|
|
554
|
+
tool(({ job_id }) =>
|
|
555
|
+
leafwrightRequest(`/api/v1/jobs/${encodeURIComponent(job_id)}/logs`, { method: "GET" })
|
|
556
|
+
)
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
server.registerTool(
|
|
560
|
+
"leafwright_get_usage",
|
|
561
|
+
{
|
|
562
|
+
title: "Get usage and credits",
|
|
563
|
+
description: "Get current billing-period usage for the active project.",
|
|
564
|
+
inputSchema: { project_id: z.string().optional().describe("Optional project override.") },
|
|
565
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
566
|
+
},
|
|
567
|
+
tool(({ project_id }) => leafwrightRequest(withProjectQuery("/api/v1/usage", project_id), { method: "GET" }))
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
server.registerTool(
|
|
571
|
+
"leafwright_estimate_usage_cost",
|
|
572
|
+
{
|
|
573
|
+
title: "Estimate usage cost",
|
|
574
|
+
description:
|
|
575
|
+
"Estimate credit usage and overage cost for a planned batch of renders against the active project's plan and current billing period.",
|
|
576
|
+
inputSchema: {
|
|
577
|
+
renders: z.number().int().min(1).describe("Number of planned renders."),
|
|
578
|
+
pages_per_render: z
|
|
579
|
+
.number()
|
|
580
|
+
.int()
|
|
581
|
+
.min(1)
|
|
582
|
+
.optional()
|
|
583
|
+
.describe("Expected pages per rendered document. Defaults to 1."),
|
|
584
|
+
project_id: z.string().optional().describe("Optional project override.")
|
|
585
|
+
},
|
|
586
|
+
annotations: { readOnlyHint: true, openWorldHint: true }
|
|
587
|
+
},
|
|
588
|
+
tool(({ renders, pages_per_render, project_id }) => {
|
|
589
|
+
const params = new URLSearchParams({ renders: String(renders) });
|
|
590
|
+
|
|
591
|
+
if (pages_per_render !== undefined) {
|
|
592
|
+
params.set("pages_per_render", String(pages_per_render));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return leafwrightRequest(withProjectQuery(`/api/v1/usage/estimate?${params}`, project_id), {
|
|
596
|
+
method: "GET"
|
|
597
|
+
});
|
|
598
|
+
})
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const HTML_RULES =
|
|
603
|
+
"Full HTML documents are supported: doctype, html, head, meta, title, body, style, plus text, layout, table, and img tags. Rejected: <script>, <iframe>, <object>, <embed>, <form>, <link>, inline <svg> markup, inline event handlers, and external http(s) asset URLs. Embed images — including SVGs — as base64 data URIs in <img> src (data:image/png, jpeg, gif, webp, or svg+xml;base64,...), relative paths, or cid: references. CSS must not use @import or external url().";
|
|
604
|
+
|
|
605
|
+
function registerWriteTools(server) {
|
|
606
|
+
server.registerTool(
|
|
607
|
+
"leafwright_render_html",
|
|
608
|
+
{
|
|
609
|
+
title: "Render HTML to PDF",
|
|
610
|
+
description:
|
|
611
|
+
`Render raw HTML into a PDF. Returns job metadata and a signed download URL for sync renders. ${HTML_RULES}`,
|
|
612
|
+
inputSchema: {
|
|
613
|
+
html: z.string().min(1).describe("Raw HTML to render. Follows the HTML rules in the tool description."),
|
|
614
|
+
filename: z.string().optional().describe("Optional output filename."),
|
|
615
|
+
async: z.boolean().optional().describe("Queue the render instead of waiting for completion."),
|
|
616
|
+
project_id: z.string().optional().describe("Optional project override.")
|
|
617
|
+
},
|
|
618
|
+
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
619
|
+
},
|
|
620
|
+
tool(({ html, filename, async = false, project_id }) =>
|
|
621
|
+
leafwrightRequest("/api/v1/pdfs", {
|
|
622
|
+
method: "POST",
|
|
623
|
+
body: JSON.stringify(withProjectBody({
|
|
624
|
+
source: { type: "html", html },
|
|
625
|
+
filename,
|
|
626
|
+
delivery: { mode: async ? "async" : "sync", return: "url" }
|
|
627
|
+
}, project_id))
|
|
628
|
+
})
|
|
629
|
+
)
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
server.registerTool(
|
|
633
|
+
"leafwright_render_url",
|
|
634
|
+
{
|
|
635
|
+
title: "Render URL to PDF",
|
|
636
|
+
description: "Render a public HTTP(S) URL into a PDF. Leafwright applies SSRF protections before rendering.",
|
|
637
|
+
inputSchema: {
|
|
638
|
+
url: z.string().url().describe("Public HTTP(S) URL to render."),
|
|
639
|
+
filename: z.string().optional().describe("Optional output filename."),
|
|
640
|
+
async: z.boolean().optional().describe("Queue the render instead of waiting for completion."),
|
|
641
|
+
project_id: z.string().optional().describe("Optional project override.")
|
|
642
|
+
},
|
|
643
|
+
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
644
|
+
},
|
|
645
|
+
tool(({ url, filename, async = false, project_id }) =>
|
|
646
|
+
leafwrightRequest("/api/v1/pdfs", {
|
|
647
|
+
method: "POST",
|
|
648
|
+
body: JSON.stringify(withProjectBody({
|
|
649
|
+
source: { type: "url", url },
|
|
650
|
+
filename,
|
|
651
|
+
delivery: { mode: async ? "async" : "sync", return: "url" }
|
|
652
|
+
}, project_id))
|
|
653
|
+
})
|
|
654
|
+
)
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
server.registerTool(
|
|
658
|
+
"leafwright_render_template",
|
|
659
|
+
{
|
|
660
|
+
title: "Render template",
|
|
661
|
+
description: "Render a published Leafwright template with JSON data.",
|
|
662
|
+
inputSchema: {
|
|
663
|
+
template_id: z.string().min(1).describe("Published template ID."),
|
|
664
|
+
data: z.record(z.string(), z.unknown()).optional().describe("Template data object."),
|
|
665
|
+
filename: z.string().optional().describe("Optional output filename."),
|
|
666
|
+
template_version_id: z.string().optional().describe("Optional template version ID.")
|
|
667
|
+
},
|
|
668
|
+
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
669
|
+
},
|
|
670
|
+
tool(({ template_id, data, filename, template_version_id }) =>
|
|
671
|
+
leafwrightRequest(`/api/v1/templates/${encodeURIComponent(template_id)}/render`, {
|
|
672
|
+
method: "POST",
|
|
673
|
+
body: JSON.stringify({ data, filename, template_version_id })
|
|
674
|
+
})
|
|
675
|
+
)
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
server.registerTool(
|
|
679
|
+
"leafwright_create_template",
|
|
680
|
+
{
|
|
681
|
+
title: "Create template",
|
|
682
|
+
description: "Create a new draft template. Drafts are not published; publishing requires a human in the dashboard.",
|
|
683
|
+
inputSchema: {
|
|
684
|
+
name: z.string().min(1).describe("Template name."),
|
|
685
|
+
html: z.string().optional().describe("Draft HTML (Handlebars supported). Follows the HTML rules in the leafwright://docs resource."),
|
|
686
|
+
css: z.string().optional().describe("Draft CSS."),
|
|
687
|
+
sample_data: z.record(z.string(), z.unknown()).optional().describe("Sample data."),
|
|
688
|
+
project_id: z.string().optional().describe("Optional project override.")
|
|
689
|
+
},
|
|
690
|
+
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
691
|
+
},
|
|
692
|
+
tool(({ name, html, css, sample_data, project_id }) =>
|
|
693
|
+
leafwrightRequest("/api/v1/templates", {
|
|
694
|
+
method: "POST",
|
|
695
|
+
body: JSON.stringify(withProjectBody({ name, html, css, sample_data }, project_id))
|
|
696
|
+
})
|
|
697
|
+
)
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
server.registerTool(
|
|
701
|
+
"leafwright_update_template_draft",
|
|
702
|
+
{
|
|
703
|
+
title: "Update template draft",
|
|
704
|
+
description: "Update a template's draft HTML, CSS, name, or sample data.",
|
|
705
|
+
inputSchema: {
|
|
706
|
+
template_id: z.string().min(1).describe("Template ID."),
|
|
707
|
+
name: z.string().optional().describe("New template name."),
|
|
708
|
+
html: z.string().optional().describe("New draft HTML. Follows the HTML rules in the leafwright://docs resource."),
|
|
709
|
+
css: z.string().optional().describe("New draft CSS."),
|
|
710
|
+
sample_data: z.record(z.string(), z.unknown()).optional().describe("New sample data.")
|
|
711
|
+
},
|
|
712
|
+
annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
713
|
+
},
|
|
714
|
+
tool(({ template_id, name, html, css, sample_data }) =>
|
|
715
|
+
leafwrightRequest(`/api/v1/templates/${encodeURIComponent(template_id)}`, {
|
|
716
|
+
method: "PATCH",
|
|
717
|
+
body: JSON.stringify({ name, html, css, sample_data })
|
|
718
|
+
})
|
|
719
|
+
)
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
server.registerTool(
|
|
723
|
+
"leafwright_ai_generate_template",
|
|
724
|
+
{
|
|
725
|
+
title: "AI generate template",
|
|
726
|
+
description: "Create an AI-assisted template draft from a prompt.",
|
|
727
|
+
inputSchema: {
|
|
728
|
+
prompt: z.string().min(1).describe("Template generation instructions."),
|
|
729
|
+
document_type: z.string().optional().describe("Document type."),
|
|
730
|
+
sample_data: z.record(z.string(), z.unknown()).optional().describe("Sample data."),
|
|
731
|
+
name: z.string().optional().describe("Optional template name."),
|
|
732
|
+
project_id: z.string().optional().describe("Optional project override.")
|
|
733
|
+
},
|
|
734
|
+
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
735
|
+
},
|
|
736
|
+
tool(({ prompt, document_type, sample_data, name, project_id }) =>
|
|
737
|
+
leafwrightRequest("/api/v1/templates/ai/generate", {
|
|
738
|
+
method: "POST",
|
|
739
|
+
body: JSON.stringify(withProjectBody({ prompt, document_type, sample_data, name }, project_id))
|
|
740
|
+
})
|
|
741
|
+
)
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
server.registerTool(
|
|
745
|
+
"leafwright_ai_revise_template",
|
|
746
|
+
{
|
|
747
|
+
title: "AI revise template",
|
|
748
|
+
description: "Revise a template's draft from a natural-language instruction.",
|
|
749
|
+
inputSchema: {
|
|
750
|
+
template_id: z.string().min(1).describe("Template ID."),
|
|
751
|
+
instruction: z.string().min(1).describe("What to change."),
|
|
752
|
+
apply: z.boolean().optional().describe("Apply the revision to the draft.")
|
|
753
|
+
},
|
|
754
|
+
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
755
|
+
},
|
|
756
|
+
tool(({ template_id, instruction, apply }) =>
|
|
757
|
+
leafwrightRequest(`/api/v1/templates/${encodeURIComponent(template_id)}/ai/revise`, {
|
|
758
|
+
method: "POST",
|
|
759
|
+
body: JSON.stringify({ instruction, apply })
|
|
760
|
+
})
|
|
761
|
+
)
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
server.registerTool(
|
|
765
|
+
"leafwright_ai_fix_from_logs",
|
|
766
|
+
{
|
|
767
|
+
title: "AI fix template from render logs",
|
|
768
|
+
description: "Repair a template's draft using the render logs of a failed or wrong-looking job.",
|
|
769
|
+
inputSchema: {
|
|
770
|
+
template_id: z.string().min(1).describe("Template ID to repair."),
|
|
771
|
+
job_id: z.string().min(1).describe("Render job ID whose logs explain the failure."),
|
|
772
|
+
apply: z.boolean().optional().describe("Apply the fix to the draft.")
|
|
773
|
+
},
|
|
774
|
+
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
775
|
+
},
|
|
776
|
+
tool(({ template_id, job_id, apply }) =>
|
|
777
|
+
leafwrightRequest(`/api/v1/templates/${encodeURIComponent(template_id)}/ai/fix-from-logs`, {
|
|
778
|
+
method: "POST",
|
|
779
|
+
body: JSON.stringify({ job_id, apply })
|
|
780
|
+
})
|
|
781
|
+
)
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const DOCS_TEXT = `# Leafwright MCP
|
|
786
|
+
|
|
787
|
+
Run leafwright_login to authenticate in a browser, choose an organization and a
|
|
788
|
+
default project, then call leafwright_auth_status after approval.
|
|
789
|
+
|
|
790
|
+
Existing LEAFWRIGHT_API_KEY and LEAFWRIGHT_MCP_TOKEN environment variables are
|
|
791
|
+
still supported for automation.
|
|
792
|
+
|
|
793
|
+
## HTML rules
|
|
794
|
+
|
|
795
|
+
Leafwright validates all HTML before rendering. The same rules apply to
|
|
796
|
+
leafwright_render_html, template create/update, and template validation.
|
|
797
|
+
|
|
798
|
+
Supported:
|
|
799
|
+
- Full documents: doctype, html, head, meta, title, body, and <style> blocks.
|
|
800
|
+
- Text, heading, list, table, and layout tags (div, section, header, footer,
|
|
801
|
+
main, article, figure).
|
|
802
|
+
- <img> with base64 data URIs (data:image/png, jpeg, gif, webp, or svg+xml),
|
|
803
|
+
relative paths, or cid: references.
|
|
804
|
+
- class, style, id, and standard layout/table/meta/image attributes, plus
|
|
805
|
+
aria-* and data-*.
|
|
806
|
+
- Handlebars placeholders like {{customer.name}} in templates.
|
|
807
|
+
|
|
808
|
+
Rejected:
|
|
809
|
+
- <script>, <iframe>, <object>, <embed>, <form>, <link>, and inline event
|
|
810
|
+
handlers (onclick and similar).
|
|
811
|
+
- Inline <svg> markup (svg, path, rect, g, and the rest of the SVG element
|
|
812
|
+
family). Encode the SVG as a base64 data URI and reference it from an
|
|
813
|
+
<img> src instead: <img src="data:image/svg+xml;base64,...">. The data URI
|
|
814
|
+
must be base64 — URL-encoded SVG data URIs are rejected.
|
|
815
|
+
- SVG in CSS url() backgrounds — use an <img> element instead.
|
|
816
|
+
- External http(s) URLs in src attributes or CSS url() — the renderer never
|
|
817
|
+
fetches remote assets for HTML sources. Embed images as data URIs instead.
|
|
818
|
+
- CSS @import.
|
|
819
|
+
|
|
820
|
+
Size limits: HTML 250 KB, CSS 120 KB, up to 200 pages per PDF.
|
|
821
|
+
|
|
822
|
+
To render an existing public web page with its remote assets, use
|
|
823
|
+
leafwright_render_url instead — that path fetches the page with SSRF
|
|
824
|
+
protections.`;
|
|
825
|
+
|
|
826
|
+
function registerDocsResource(server) {
|
|
827
|
+
server.registerResource(
|
|
828
|
+
"leafwright-docs",
|
|
829
|
+
"leafwright://docs",
|
|
830
|
+
{
|
|
831
|
+
title: "Leafwright MCP quickstart",
|
|
832
|
+
description: "How to authenticate and use Leafwright MCP.",
|
|
833
|
+
mimeType: "text/markdown"
|
|
834
|
+
},
|
|
835
|
+
async (uri) => ({
|
|
836
|
+
contents: [{ uri: uri.href, mimeType: "text/markdown", text: DOCS_TEXT }]
|
|
837
|
+
})
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function registerAuthenticatedResources(server) {
|
|
842
|
+
server.registerResource(
|
|
843
|
+
"leafwright-templates",
|
|
844
|
+
"leafwright://templates",
|
|
845
|
+
{
|
|
846
|
+
title: "Project templates",
|
|
847
|
+
description: "Templates available to the active project.",
|
|
848
|
+
mimeType: "application/json"
|
|
849
|
+
},
|
|
850
|
+
async (uri) => {
|
|
851
|
+
const data = await leafwrightRequest(withProjectQuery("/api/v1/templates"), { method: "GET" });
|
|
852
|
+
return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(data, null, 2) }] };
|
|
853
|
+
}
|
|
854
|
+
);
|
|
855
|
+
|
|
856
|
+
server.registerResource(
|
|
857
|
+
"leafwright-usage",
|
|
858
|
+
"leafwright://usage",
|
|
859
|
+
{
|
|
860
|
+
title: "Current usage",
|
|
861
|
+
description: "Credits used and remaining for the active project.",
|
|
862
|
+
mimeType: "application/json"
|
|
863
|
+
},
|
|
864
|
+
async (uri) => {
|
|
865
|
+
const data = await leafwrightRequest(withProjectQuery("/api/v1/usage"), { method: "GET" });
|
|
866
|
+
return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(data, null, 2) }] };
|
|
867
|
+
}
|
|
868
|
+
);
|
|
869
|
+
|
|
870
|
+
server.registerResource(
|
|
871
|
+
"leafwright-recent-jobs",
|
|
872
|
+
"leafwright://jobs/recent",
|
|
873
|
+
{
|
|
874
|
+
title: "Recent render jobs",
|
|
875
|
+
description: "The most recent render jobs for the active project.",
|
|
876
|
+
mimeType: "application/json"
|
|
877
|
+
},
|
|
878
|
+
async (uri) => {
|
|
879
|
+
const data = await leafwrightRequest(withProjectQuery("/api/v1/jobs?limit=20"), { method: "GET" });
|
|
880
|
+
return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(data, null, 2) }] };
|
|
881
|
+
}
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
server.registerResource(
|
|
885
|
+
"leafwright-job-logs",
|
|
886
|
+
new ResourceTemplate("leafwright://jobs/{jobId}/logs", { list: undefined }),
|
|
887
|
+
{
|
|
888
|
+
title: "Render job logs",
|
|
889
|
+
description: "Render logs for a specific job, useful for debugging failures.",
|
|
890
|
+
mimeType: "application/json"
|
|
891
|
+
},
|
|
892
|
+
async (uri, { jobId }) => {
|
|
893
|
+
const data = await leafwrightRequest(`/api/v1/jobs/${encodeURIComponent(jobId)}/logs`, {
|
|
894
|
+
method: "GET"
|
|
895
|
+
});
|
|
896
|
+
return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(data, null, 2) }] };
|
|
897
|
+
}
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function userPrompt(text) {
|
|
902
|
+
return { messages: [{ role: "user", content: { type: "text", text } }] };
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function registerPrompts(server) {
|
|
906
|
+
server.registerPrompt(
|
|
907
|
+
"create_invoice_template",
|
|
908
|
+
{
|
|
909
|
+
title: "Create an invoice template",
|
|
910
|
+
description: "Generate a clean invoice template with Leafwright AI.",
|
|
911
|
+
argsSchema: { details: z.string().optional().describe("Line items, fields, branding notes, etc.") }
|
|
912
|
+
},
|
|
913
|
+
({ details }) =>
|
|
914
|
+
userPrompt(
|
|
915
|
+
`Create a professional invoice template in Leafwright using leafwright_ai_generate_template with document_type "invoice".${
|
|
916
|
+
details ? `\n\nSpecifics:\n${details}` : ""
|
|
917
|
+
}\n\nRender a preview and debug with leafwright_get_job_logs plus leafwright_ai_fix_from_logs if needed. Leave it as a draft.`
|
|
918
|
+
)
|
|
919
|
+
);
|
|
920
|
+
|
|
921
|
+
server.registerPrompt(
|
|
922
|
+
"create_report_template",
|
|
923
|
+
{
|
|
924
|
+
title: "Create a report template",
|
|
925
|
+
description: "Generate a multi-section report template.",
|
|
926
|
+
argsSchema: { details: z.string().optional().describe("Sections, charts, branding notes, etc.") }
|
|
927
|
+
},
|
|
928
|
+
({ details }) =>
|
|
929
|
+
userPrompt(
|
|
930
|
+
`Create a multi-page report template in Leafwright using leafwright_ai_generate_template with document_type "report".${
|
|
931
|
+
details ? `\n\nSpecifics:\n${details}` : ""
|
|
932
|
+
}\n\nRender a preview, debug if needed, and leave it as a draft.`
|
|
933
|
+
)
|
|
934
|
+
);
|
|
935
|
+
|
|
936
|
+
server.registerPrompt(
|
|
937
|
+
"create_template_from_prompt",
|
|
938
|
+
{
|
|
939
|
+
title: "Create a template from a description",
|
|
940
|
+
description: "Generate any document template from a free-text description.",
|
|
941
|
+
argsSchema: {
|
|
942
|
+
description: z.string().describe("What the document should contain and look like."),
|
|
943
|
+
document_type: z.string().optional().describe("invoice | quote | report | certificate | etc.")
|
|
944
|
+
}
|
|
945
|
+
},
|
|
946
|
+
({ description, document_type }) =>
|
|
947
|
+
userPrompt(
|
|
948
|
+
`Use leafwright_ai_generate_template to create a template${
|
|
949
|
+
document_type ? ` (document_type "${document_type}")` : ""
|
|
950
|
+
} from this description:\n\n${description}\n\nThen render a preview and verify it. Leave it as a draft.`
|
|
951
|
+
)
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
server.registerPrompt(
|
|
955
|
+
"debug_pdf_layout",
|
|
956
|
+
{
|
|
957
|
+
title: "Debug a PDF layout",
|
|
958
|
+
description: "Diagnose and fix a render that failed or looks wrong.",
|
|
959
|
+
argsSchema: {
|
|
960
|
+
job_id: z.string().describe("The render job that failed or looked wrong."),
|
|
961
|
+
template_id: z.string().optional().describe("If the render came from a template, its ID.")
|
|
962
|
+
}
|
|
963
|
+
},
|
|
964
|
+
({ job_id, template_id }) =>
|
|
965
|
+
userPrompt(
|
|
966
|
+
`Call leafwright_get_job and leafwright_get_job_logs with job_id "${job_id}", explain the root cause, then ${
|
|
967
|
+
template_id
|
|
968
|
+
? `call leafwright_ai_fix_from_logs with template_id "${template_id}" and job_id "${job_id}".`
|
|
969
|
+
: "propose the HTML/CSS change that fixes it."
|
|
970
|
+
}`
|
|
971
|
+
)
|
|
972
|
+
);
|
|
973
|
+
|
|
974
|
+
server.registerPrompt(
|
|
975
|
+
"explain_render_failure",
|
|
976
|
+
{
|
|
977
|
+
title: "Explain a render failure",
|
|
978
|
+
description: "Plain-language explanation of why a render failed.",
|
|
979
|
+
argsSchema: { job_id: z.string().describe("The failed render job ID.") }
|
|
980
|
+
},
|
|
981
|
+
({ job_id }) =>
|
|
982
|
+
userPrompt(
|
|
983
|
+
`Call leafwright_get_job_logs with job_id "${job_id}" and explain why the render failed and exactly what to change.`
|
|
984
|
+
)
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
server.registerPrompt(
|
|
988
|
+
"integrate_nextjs",
|
|
989
|
+
{
|
|
990
|
+
title: "Integrate Leafwright into a Next.js app",
|
|
991
|
+
description: "Add server-side PDF generation to a Next.js project.",
|
|
992
|
+
argsSchema: {}
|
|
993
|
+
},
|
|
994
|
+
() =>
|
|
995
|
+
userPrompt(
|
|
996
|
+
"Add PDF generation to this Next.js app using Leafwright instead of Puppeteer. Use LEAFWRIGHT_API_KEY or Leafwright MCP auth and verify with leafwright_render_html."
|
|
997
|
+
)
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
server.registerPrompt(
|
|
1001
|
+
"migrate_from_puppeteer",
|
|
1002
|
+
{
|
|
1003
|
+
title: "Migrate from Puppeteer to Leafwright",
|
|
1004
|
+
description: "Replace headless-Chrome PDF code with the Leafwright API.",
|
|
1005
|
+
argsSchema: {}
|
|
1006
|
+
},
|
|
1007
|
+
() =>
|
|
1008
|
+
userPrompt(
|
|
1009
|
+
"Find Puppeteer PDF generation and replace it with Leafwright /api/v1/pdfs calls. Use leafwright_render_html to confirm output before deleting old code."
|
|
1010
|
+
)
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function sendListChanged(server) {
|
|
1015
|
+
try {
|
|
1016
|
+
server.sendToolListChanged();
|
|
1017
|
+
server.sendResourceListChanged();
|
|
1018
|
+
server.sendPromptListChanged();
|
|
1019
|
+
} catch {
|
|
1020
|
+
// The server may not be connected yet during startup registration.
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
export function createLeafwrightMcpServer(options = {}) {
|
|
1025
|
+
runtimeOverrides = options;
|
|
1026
|
+
const server = new McpServer({
|
|
1027
|
+
name: "leafwright",
|
|
1028
|
+
version: "0.3.0"
|
|
1029
|
+
});
|
|
1030
|
+
let docsRegistered = false;
|
|
1031
|
+
let resourcesRegistered = false;
|
|
1032
|
+
let promptsRegistered = false;
|
|
1033
|
+
let fullToolsRegistered = false;
|
|
1034
|
+
|
|
1035
|
+
function ensureDocsResource() {
|
|
1036
|
+
if (docsRegistered) {
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
registerDocsResource(server);
|
|
1041
|
+
docsRegistered = true;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function ensureAuthenticatedResources() {
|
|
1045
|
+
if (resourcesRegistered) {
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
registerAuthenticatedResources(server);
|
|
1050
|
+
resourcesRegistered = true;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function ensurePrompts() {
|
|
1054
|
+
if (promptsRegistered) {
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
registerPrompts(server);
|
|
1059
|
+
promptsRegistered = true;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function ensureFullTools() {
|
|
1063
|
+
if (fullToolsRegistered || !isAuthenticated()) {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
registerContextTools(server);
|
|
1068
|
+
registerReadTools(server);
|
|
1069
|
+
ensureDocsResource();
|
|
1070
|
+
ensureAuthenticatedResources();
|
|
1071
|
+
ensurePrompts();
|
|
1072
|
+
|
|
1073
|
+
if (!config().readOnly) {
|
|
1074
|
+
registerWriteTools(server);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
fullToolsRegistered = true;
|
|
1078
|
+
sendListChanged(server);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
registerAuthTools(server, ensureFullTools);
|
|
1082
|
+
ensureDocsResource();
|
|
1083
|
+
|
|
1084
|
+
if (isAuthenticated()) {
|
|
1085
|
+
ensureFullTools();
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
return server;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
async function main() {
|
|
1092
|
+
const server = createLeafwrightMcpServer();
|
|
1093
|
+
const transport = new StdioServerTransport();
|
|
1094
|
+
await server.connect(transport);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
1098
|
+
await main();
|
|
1099
|
+
}
|