jxp 4.2.0 → 5.0.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/.env.sample +16 -0
- package/README.md +2 -0
- package/dist/bin/mcp-stdio-bridge.d.ts +3 -0
- package/dist/bin/mcp-stdio-bridge.d.ts.map +1 -0
- package/dist/bin/mcp-stdio-bridge.js +87 -0
- package/dist/bin/mcp-stdio-bridge.js.map +1 -0
- package/dist/bin/server.d.ts.map +1 -1
- package/dist/bin/server.js +2 -1
- package/dist/bin/server.js.map +1 -1
- package/dist/libs/docs-auth.d.ts.map +1 -1
- package/dist/libs/docs-auth.js +4 -2
- package/dist/libs/docs-auth.js.map +1 -1
- package/dist/libs/docs.js +73 -42
- package/dist/libs/docs.js.map +1 -1
- package/dist/libs/jxp.d.ts.map +1 -1
- package/dist/libs/jxp.js +16 -319
- package/dist/libs/jxp.js.map +1 -1
- package/dist/libs/mcp/auth.d.ts +4 -0
- package/dist/libs/mcp/auth.d.ts.map +1 -0
- package/dist/libs/mcp/auth.js +49 -0
- package/dist/libs/mcp/auth.js.map +1 -0
- package/dist/libs/mcp/config.d.ts +21 -0
- package/dist/libs/mcp/config.d.ts.map +1 -0
- package/dist/libs/mcp/config.js +66 -0
- package/dist/libs/mcp/config.js.map +1 -0
- package/dist/libs/mcp/context.d.ts +5 -0
- package/dist/libs/mcp/context.d.ts.map +1 -0
- package/dist/libs/mcp/context.js +14 -0
- package/dist/libs/mcp/context.js.map +1 -0
- package/dist/libs/mcp/describe.d.ts +13 -0
- package/dist/libs/mcp/describe.d.ts.map +1 -0
- package/dist/libs/mcp/describe.js +36 -0
- package/dist/libs/mcp/describe.js.map +1 -0
- package/dist/libs/mcp/docs_auth.d.ts +5 -0
- package/dist/libs/mcp/docs_auth.d.ts.map +1 -0
- package/dist/libs/mcp/docs_auth.js +36 -0
- package/dist/libs/mcp/docs_auth.js.map +1 -0
- package/dist/libs/mcp/dynamic_import.d.ts +6 -0
- package/dist/libs/mcp/dynamic_import.d.ts.map +1 -0
- package/dist/libs/mcp/dynamic_import.js +12 -0
- package/dist/libs/mcp/dynamic_import.js.map +1 -0
- package/dist/libs/mcp/execute_tool.d.ts +10 -0
- package/dist/libs/mcp/execute_tool.d.ts.map +1 -0
- package/dist/libs/mcp/execute_tool.js +65 -0
- package/dist/libs/mcp/execute_tool.js.map +1 -0
- package/dist/libs/mcp/guides/default.md +67 -0
- package/dist/libs/mcp/guides.d.ts +14 -0
- package/dist/libs/mcp/guides.d.ts.map +1 -0
- package/dist/libs/mcp/guides.js +127 -0
- package/dist/libs/mcp/guides.js.map +1 -0
- package/dist/libs/mcp/model_visibility.d.ts +31 -0
- package/dist/libs/mcp/model_visibility.d.ts.map +1 -0
- package/dist/libs/mcp/model_visibility.js +73 -0
- package/dist/libs/mcp/model_visibility.js.map +1 -0
- package/dist/libs/mcp/mount.d.ts +10 -0
- package/dist/libs/mcp/mount.d.ts.map +1 -0
- package/dist/libs/mcp/mount.js +68 -0
- package/dist/libs/mcp/mount.js.map +1 -0
- package/dist/libs/mcp/read_service.d.ts +23 -0
- package/dist/libs/mcp/read_service.d.ts.map +1 -0
- package/dist/libs/mcp/read_service.js +141 -0
- package/dist/libs/mcp/read_service.js.map +1 -0
- package/dist/libs/mcp/server.d.ts +23 -0
- package/dist/libs/mcp/server.d.ts.map +1 -0
- package/dist/libs/mcp/server.js +111 -0
- package/dist/libs/mcp/server.js.map +1 -0
- package/dist/libs/read_handlers.d.ts +13 -0
- package/dist/libs/read_handlers.d.ts.map +1 -0
- package/dist/libs/read_handlers.js +381 -0
- package/dist/libs/read_handlers.js.map +1 -0
- package/dist/libs/schema_serialize.d.ts +3 -0
- package/dist/libs/schema_serialize.d.ts.map +1 -0
- package/dist/libs/schema_serialize.js +45 -0
- package/dist/libs/schema_serialize.js.map +1 -0
- package/dist/types/jxp-config.d.ts +7 -0
- package/dist/types/jxp-config.d.ts.map +1 -1
- package/docs/changelog.md +35 -0
- package/docs/index.md +1 -0
- package/docs/mcp.md +134 -0
- package/mkdocs.yml +1 -0
- package/package.json +11 -5
- package/templates/assets/docs.css +107 -0
- package/templates/assets/mcp-console.js +343 -0
- package/templates/index.pug +37 -0
- package/templates/mcp.pug +112 -0
- package/templates/navbar.pug +2 -0
- package/templates/sidebar.pug +4 -0
package/docs/mcp.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# MCP (Model Context Protocol)
|
|
2
|
+
|
|
3
|
+
JXP can expose a **read-only** MCP server on the same HTTP port as the REST API. This lets AI clients (Cursor, LM Studio, RevEngine2 admin proxy) query your data with the same permissions as the REST API.
|
|
4
|
+
|
|
5
|
+
## Enable
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
MCP_ENABLED=true
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Optional settings (all prefixed `MCP_`):
|
|
12
|
+
|
|
13
|
+
| Variable | Default | Purpose |
|
|
14
|
+
|----------|---------|---------|
|
|
15
|
+
| `MCP_PATH` | `/mcp` | HTTP endpoint |
|
|
16
|
+
| `MCP_REQUIRE_API_KEY` | `true` | Reject unauthenticated MCP |
|
|
17
|
+
| `MCP_MODEL_WHITELIST` | _(empty)_ | Expose default-hidden models (e.g. `user`) |
|
|
18
|
+
| `MCP_MODEL_BLACKLIST` | _(empty)_ | Always hide models |
|
|
19
|
+
| `MCP_DEFAULT_HIDDEN_MODELS` | builtin auth slugs | Override default-deny list |
|
|
20
|
+
| `MCP_DEFAULT_LIMIT` | `20` | Default row limit |
|
|
21
|
+
| `MCP_MAX_LIMIT` | `100` | Hard cap on `limit` |
|
|
22
|
+
| `MCP_MAX_RESPONSE_SIZE` | `256kb` | JSON response cap |
|
|
23
|
+
| `MCP_MAX_CSV_SIZE` | `512kb` | CSV export cap |
|
|
24
|
+
| `MCP_TRUNCATE_STRINGS_AT` | `4000` | Truncate long string fields in JSON |
|
|
25
|
+
| `MCP_DISABLE_AUTOPOPULATE` | `true` | Ignore expensive autopopulate |
|
|
26
|
+
| `MCP_INSTRUCTIONS_APPEND` | _(empty)_ | Text appended to server instructions (initialize) |
|
|
27
|
+
| `MCP_GUIDE_FILES` | _(empty)_ | Comma-separated markdown files merged into `jxp-guide` resource |
|
|
28
|
+
|
|
29
|
+
## LLM guidance
|
|
30
|
+
|
|
31
|
+
On connect, the MCP `initialize` response includes **server instructions** — a short workflow summary injected for the LLM.
|
|
32
|
+
|
|
33
|
+
For the full guide, read the fixed MCP resource **`jxp-guide`** (`jxp://guide`, markdown). Content is merged in order:
|
|
34
|
+
|
|
35
|
+
1. Built-in [`default.md`](https://github.com/WorkSpaceMan/jxp/blob/main/src/libs/mcp/guides/default.md)
|
|
36
|
+
2. `JXPConfig.mcp.guideFiles` (app config)
|
|
37
|
+
3. `MCP_GUIDE_FILES` (env)
|
|
38
|
+
|
|
39
|
+
### App extension (`JXPConfig.mcp`)
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
apiconfig.mcp = {
|
|
43
|
+
instructions: "Short app-specific note appended to server instructions.",
|
|
44
|
+
guideFiles: [path.join(__dirname, "../../mcp/guide.md")],
|
|
45
|
+
};
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Example: [revengine-api `mcp/guide.md`](https://github.com/WorkSpaceMan/revengine-api) adds domain context for `reader`, `segment`, etc.
|
|
49
|
+
|
|
50
|
+
## Tools (fixed)
|
|
51
|
+
|
|
52
|
+
1. **`jxp_list_models`** — models the caller can read via MCP
|
|
53
|
+
2. **`jxp_describe_model`** — field types, links, populate hints
|
|
54
|
+
3. **`jxp_find`** — list or get by `id`; supports `filter`, `search`, `populate`, `fields`, `sort`, `limit`, `page`
|
|
55
|
+
4. **`jxp_count`** — count with optional `filter` / `search`
|
|
56
|
+
5. **`jxp_export_csv`** — CSV text export (use `fields` + low `limit`)
|
|
57
|
+
|
|
58
|
+
Tools never change when you add models — agents call `jxp_list_models` first.
|
|
59
|
+
|
|
60
|
+
## Authentication
|
|
61
|
+
|
|
62
|
+
Send the user's API key on every MCP HTTP request:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
X-API-Key: <apikey>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Bearer tokens also work. MCP uses the same permission rules as `GET /api/:model`.
|
|
69
|
+
|
|
70
|
+
## Docs playground
|
|
71
|
+
|
|
72
|
+
When `MCP_ENABLED=true`, the built-in docs UI includes **`/docs/mcp`** — a chat-style assistant that calls the same five MCP tools (protected by the same login as **Browse API**). Use the API key field in the top bar or sign in; tool calls go through `POST /docs/mcp/call` with your permissions.
|
|
73
|
+
|
|
74
|
+
Default-hidden models (unless whitelisted): built-in auth collections (`user`, `apikey`, `token`, etc.) and schemas with `internal: true`.
|
|
75
|
+
|
|
76
|
+
## Cursor / LM Studio (stdio)
|
|
77
|
+
|
|
78
|
+
Use the `jxp-mcp` bridge (proxies stdio → HTTP, including tools, resources, and instructions):
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"mcpServers": {
|
|
83
|
+
"jxp": {
|
|
84
|
+
"command": "npx",
|
|
85
|
+
"args": ["-y", "jxp-mcp"],
|
|
86
|
+
"env": {
|
|
87
|
+
"JXP_URL": "http://localhost:4001",
|
|
88
|
+
"JXP_API_KEY": "your-api-key"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
For Cursor, direct HTTP MCP also works and receives instructions automatically:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"mcpServers": {
|
|
100
|
+
"jxp": {
|
|
101
|
+
"url": "http://localhost:4001/mcp",
|
|
102
|
+
"headers": { "X-API-Key": "your-api-key" }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## RevEngine2 / server proxy
|
|
109
|
+
|
|
110
|
+
Backend connects to `{API_URL}/mcp` with the logged-in user's API key and proxies MCP between the admin UI and JXP. JXP's own docs UI also exposes `/docs/mcp` for local exploration.
|
|
111
|
+
|
|
112
|
+
## Deploying `jxp-mcp`
|
|
113
|
+
|
|
114
|
+
`jxp-mcp` is **not** a separate npm package. It ships as a **CLI binary on the `jxp` package** (since v5.0.0):
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npm install jxp@5 # or depend on jxp in your app
|
|
118
|
+
npx jxp-mcp # stdio MCP → your HTTP /mcp endpoint
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
| Component | Where it runs | Role |
|
|
122
|
+
|-----------|---------------|------|
|
|
123
|
+
| **JXP HTTP API** | Your server (e.g. RevEngine Docker on port 4001) | Serves REST + `/mcp` when `MCP_ENABLED=true` |
|
|
124
|
+
| **`jxp-mcp` bridge** | Developer's machine (Cursor, LM Studio) | stdio proxy; needs `JXP_URL` + `JXP_API_KEY` pointing at the HTTP server |
|
|
125
|
+
|
|
126
|
+
**Production API containers** do not need to run `jxp-mcp`. Enable MCP on the API (`MCP_ENABLED=true`) and connect from the IDE via HTTP or stdio.
|
|
127
|
+
|
|
128
|
+
**Publish order:** `npm publish` **jxp@5** first, then bump app dependencies (e.g. `revengine-api` `jxp: ^5.0.0`) and rebuild Docker images.
|
|
129
|
+
|
|
130
|
+
## v1 limitations
|
|
131
|
+
|
|
132
|
+
- Read-only (no mutations)
|
|
133
|
+
- No `POST /query`, `/aggregate`, or `/call`
|
|
134
|
+
- No per-model `mcp:` schema flag (env lists only)
|
package/mkdocs.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jxp",
|
|
3
3
|
"description": "An opinionated RESTful API library based on Mongoose and Restify. Make an API by just writing Mongoose models.",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "5.0.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "dist/libs/jxp.js",
|
|
7
7
|
"types": "dist/libs/jxp.d.ts",
|
|
@@ -50,12 +50,12 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
|
-
"build": "tsc -p tsconfig.build.json",
|
|
53
|
+
"build": "tsc -p tsconfig.build.json && node -e \"require('fs').cpSync('src/libs/mcp/guides','dist/libs/mcp/guides',{recursive:true})\"",
|
|
54
54
|
"prepublishOnly": "npm run build",
|
|
55
55
|
"prepare": "node -e \"require('fs').existsSync('src')&&require('child_process').execSync('npm run build',{stdio:'inherit'})\"",
|
|
56
56
|
"start": "node dist/bin/server.js",
|
|
57
57
|
"dev": "nodemon --watch src --ext ts --exec 'npm run build && node dist/bin/server.js'",
|
|
58
|
-
"test": "npm run build && mocha --require test/env.js test/builtin_models.test.js test/bulkwrite_guard.test.js test/index_diagnostics.test.js test/parse_byte_size.test.js test/query_limits.test.js test/request_log.test.js test/test.js --timeout 1000 -b --exit",
|
|
58
|
+
"test": "npm run build && mocha --require test/env.js test/builtin_models.test.js test/bulkwrite_guard.test.js test/index_diagnostics.test.js test/mcp_guides.test.js test/mcp_visibility.test.js test/parse_byte_size.test.js test/query_limits.test.js test/request_log.test.js test/test.js --timeout 1000 -b --exit",
|
|
59
59
|
"setup": "node dist/bin/setup.js",
|
|
60
60
|
"indexes": "node dist/bin/index_audit.js"
|
|
61
61
|
},
|
|
@@ -63,7 +63,8 @@
|
|
|
63
63
|
"jxp": "dist/bin/setup.js",
|
|
64
64
|
"jxp-setup": "dist/bin/setup.js",
|
|
65
65
|
"jxp-add-admin": "dist/bin/init_db.js",
|
|
66
|
-
"jxp-indexes": "dist/bin/index_audit.js"
|
|
66
|
+
"jxp-indexes": "dist/bin/index_audit.js",
|
|
67
|
+
"jxp-mcp": "dist/bin/mcp-stdio-bridge.js"
|
|
67
68
|
},
|
|
68
69
|
"repository": {
|
|
69
70
|
"type": "git",
|
|
@@ -80,7 +81,11 @@
|
|
|
80
81
|
"url": "https://github.com/WorkSpaceMan/jxp/issues"
|
|
81
82
|
},
|
|
82
83
|
"dependencies": {
|
|
84
|
+
"@cfworker/json-schema": "^4.1.1",
|
|
83
85
|
"@json2csv/plainjs": "^7.0.6",
|
|
86
|
+
"@modelcontextprotocol/client": "^2.0.0-alpha.2",
|
|
87
|
+
"@modelcontextprotocol/node": "^2.0.0-alpha.2",
|
|
88
|
+
"@modelcontextprotocol/server": "^2.0.0-alpha.2",
|
|
84
89
|
"bcryptjs": "^3.0.2",
|
|
85
90
|
"bytes": "^3.1.2",
|
|
86
91
|
"commander": "^13.1.0",
|
|
@@ -104,7 +109,8 @@
|
|
|
104
109
|
"restify-errors": "^8.0.2",
|
|
105
110
|
"traverse": "^0.6.11",
|
|
106
111
|
"underscore": "^1.13.8",
|
|
107
|
-
"ws": "^8.18.1"
|
|
112
|
+
"ws": "^8.18.1",
|
|
113
|
+
"zod": "^4.4.3"
|
|
108
114
|
},
|
|
109
115
|
"devDependencies": {
|
|
110
116
|
"@types/bcryptjs": "^2.4.6",
|
|
@@ -242,3 +242,110 @@ body.docs-body {
|
|
|
242
242
|
width: 100%;
|
|
243
243
|
max-width: 400px;
|
|
244
244
|
}
|
|
245
|
+
|
|
246
|
+
.mcp-chat-wrap {
|
|
247
|
+
border: 1px solid var(--docs-border);
|
|
248
|
+
border-radius: 0.5rem;
|
|
249
|
+
overflow: hidden;
|
|
250
|
+
background: #fafbfc;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.mcp-chat-thread {
|
|
254
|
+
max-height: 420px;
|
|
255
|
+
overflow-y: auto;
|
|
256
|
+
padding: 1rem;
|
|
257
|
+
display: flex;
|
|
258
|
+
flex-direction: column;
|
|
259
|
+
gap: 0.75rem;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.mcp-msg {
|
|
263
|
+
max-width: 92%;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.mcp-msg-user {
|
|
267
|
+
align-self: flex-end;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.mcp-msg-bot {
|
|
271
|
+
align-self: flex-start;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.mcp-msg-label {
|
|
275
|
+
font-size: 0.7rem;
|
|
276
|
+
font-weight: 600;
|
|
277
|
+
text-transform: uppercase;
|
|
278
|
+
letter-spacing: 0.04em;
|
|
279
|
+
color: #6c757d;
|
|
280
|
+
margin-bottom: 0.25rem;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.mcp-msg-body {
|
|
284
|
+
padding: 0.65rem 0.85rem;
|
|
285
|
+
border-radius: 0.5rem;
|
|
286
|
+
font-size: 0.9rem;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.mcp-msg-user .mcp-msg-body {
|
|
290
|
+
background: var(--docs-accent);
|
|
291
|
+
color: #fff;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.mcp-msg-user .mcp-msg-body code {
|
|
295
|
+
color: #fff;
|
|
296
|
+
background: rgba(255, 255, 255, 0.15);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.mcp-msg-bot .mcp-msg-body {
|
|
300
|
+
background: #fff;
|
|
301
|
+
border: 1px solid var(--docs-border);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.mcp-msg-error .mcp-msg-body {
|
|
305
|
+
border-color: #f5c2c7;
|
|
306
|
+
background: #f8d7da;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.mcp-result {
|
|
310
|
+
font-family: ui-monospace, monospace;
|
|
311
|
+
font-size: 0.78rem;
|
|
312
|
+
white-space: pre-wrap;
|
|
313
|
+
word-break: break-word;
|
|
314
|
+
max-height: 240px;
|
|
315
|
+
overflow: auto;
|
|
316
|
+
background: #212529;
|
|
317
|
+
color: #e9ecef;
|
|
318
|
+
padding: 0.75rem;
|
|
319
|
+
border-radius: 0.375rem;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.mcp-chat-input-wrap {
|
|
323
|
+
padding: 0.75rem 1rem;
|
|
324
|
+
border-top: 1px solid var(--docs-border);
|
|
325
|
+
background: #fff;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.mcp-tool-panel {
|
|
329
|
+
border: 1px solid var(--docs-border);
|
|
330
|
+
border-radius: 0.375rem;
|
|
331
|
+
padding: 1rem;
|
|
332
|
+
background: #fafbfc;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.mcp-setup-card .mcp-json-block {
|
|
336
|
+
font-family: ui-monospace, monospace;
|
|
337
|
+
font-size: 0.8rem;
|
|
338
|
+
background: #212529;
|
|
339
|
+
color: #e9ecef;
|
|
340
|
+
border-radius: 0.375rem;
|
|
341
|
+
padding: 1rem;
|
|
342
|
+
margin: 0;
|
|
343
|
+
overflow-x: auto;
|
|
344
|
+
white-space: pre;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.mcp-setup-card .mcp-copy-btn {
|
|
348
|
+
position: absolute;
|
|
349
|
+
top: 0.5rem;
|
|
350
|
+
right: 0.5rem;
|
|
351
|
+
}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const STORAGE_KEY = "jxp_docs_api_key";
|
|
5
|
+
const STORAGE_REMEMBER = "jxp_docs_remember_key";
|
|
6
|
+
|
|
7
|
+
function getApiKey() {
|
|
8
|
+
const input = document.getElementById("docs-api-key");
|
|
9
|
+
return input ? input.value.trim() : "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function loadStoredKey() {
|
|
13
|
+
if (localStorage.getItem(STORAGE_REMEMBER) !== "1") return;
|
|
14
|
+
const key = localStorage.getItem(STORAGE_KEY);
|
|
15
|
+
if (!key) return;
|
|
16
|
+
const input = document.getElementById("docs-api-key");
|
|
17
|
+
const remember = document.getElementById("docs-remember-key");
|
|
18
|
+
if (input) input.value = key;
|
|
19
|
+
if (remember) remember.checked = true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function saveKeyIfRemembered() {
|
|
23
|
+
const remember = document.getElementById("docs-remember-key");
|
|
24
|
+
const input = document.getElementById("docs-api-key");
|
|
25
|
+
if (!remember || !input) return;
|
|
26
|
+
if (remember.checked) {
|
|
27
|
+
localStorage.setItem(STORAGE_REMEMBER, "1");
|
|
28
|
+
localStorage.setItem(STORAGE_KEY, input.value.trim());
|
|
29
|
+
} else {
|
|
30
|
+
localStorage.removeItem(STORAGE_REMEMBER);
|
|
31
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
32
|
+
}
|
|
33
|
+
updateMcpConfigSnippets();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getMcpSetup() {
|
|
37
|
+
const card = document.querySelector(".mcp-setup-card");
|
|
38
|
+
const fromWindow = window.__MCP_SETUP__ || {};
|
|
39
|
+
const baseUrl =
|
|
40
|
+
fromWindow.baseUrl ||
|
|
41
|
+
card?.dataset.baseUrl ||
|
|
42
|
+
window.location.origin;
|
|
43
|
+
const mcpPath = fromWindow.mcpPath || card?.dataset.mcpPath || "/mcp";
|
|
44
|
+
const base = String(baseUrl).replace(/\/$/, "");
|
|
45
|
+
const path = mcpPath.startsWith("/") ? mcpPath : `/${mcpPath}`;
|
|
46
|
+
const mcpUrl = fromWindow.mcpUrl || card?.dataset.mcpUrl || `${base}${path}`;
|
|
47
|
+
return {
|
|
48
|
+
baseUrl: base,
|
|
49
|
+
mcpUrl,
|
|
50
|
+
serverSlug: fromWindow.serverSlug || card?.dataset.serverSlug || "jxp",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildMcpJsonConfig(kind, apiKey) {
|
|
55
|
+
const setup = getMcpSetup();
|
|
56
|
+
const key = apiKey || "YOUR_API_KEY";
|
|
57
|
+
const slug = setup.serverSlug || "jxp";
|
|
58
|
+
if (kind === "stdio") {
|
|
59
|
+
return JSON.stringify(
|
|
60
|
+
{
|
|
61
|
+
mcpServers: {
|
|
62
|
+
[slug]: {
|
|
63
|
+
command: "npx",
|
|
64
|
+
args: ["-y", "jxp-mcp"],
|
|
65
|
+
env: {
|
|
66
|
+
JXP_URL: setup.baseUrl,
|
|
67
|
+
JXP_API_KEY: key,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
null,
|
|
73
|
+
2
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return JSON.stringify(
|
|
77
|
+
{
|
|
78
|
+
mcpServers: {
|
|
79
|
+
[slug]: {
|
|
80
|
+
url: setup.mcpUrl,
|
|
81
|
+
headers: { "X-API-Key": key },
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
null,
|
|
86
|
+
2
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function updateMcpConfigSnippets() {
|
|
91
|
+
const apiKey = getApiKey();
|
|
92
|
+
const httpEl = document.getElementById("mcpJsonHttp");
|
|
93
|
+
const stdioEl = document.getElementById("mcpJsonStdio");
|
|
94
|
+
if (httpEl) httpEl.textContent = buildMcpJsonConfig("http", apiKey);
|
|
95
|
+
if (stdioEl) stdioEl.textContent = buildMcpJsonConfig("stdio", apiKey);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function copyMcpConfig(targetId, btn) {
|
|
99
|
+
const el = document.getElementById(targetId);
|
|
100
|
+
if (!el) return;
|
|
101
|
+
try {
|
|
102
|
+
await navigator.clipboard.writeText(el.textContent || "");
|
|
103
|
+
const label = btn.innerHTML;
|
|
104
|
+
btn.innerHTML = '<i class="bi bi-check2 me-1"></i>Copied';
|
|
105
|
+
setTimeout(function () {
|
|
106
|
+
btn.innerHTML = label;
|
|
107
|
+
}, 1500);
|
|
108
|
+
} catch {
|
|
109
|
+
/* ignore */
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function escapeHtml(text) {
|
|
114
|
+
return String(text)
|
|
115
|
+
.replace(/&/g, "&")
|
|
116
|
+
.replace(/</g, "<")
|
|
117
|
+
.replace(/>/g, ">")
|
|
118
|
+
.replace(/"/g, """);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function formatBody(text) {
|
|
122
|
+
if (!text) return "";
|
|
123
|
+
try {
|
|
124
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
125
|
+
} catch {
|
|
126
|
+
return text;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function appendMessage(thread, role, bodyHtml, isError) {
|
|
131
|
+
const wrap = document.createElement("div");
|
|
132
|
+
wrap.className = "mcp-msg mcp-msg-" + role + (isError ? " mcp-msg-error" : "");
|
|
133
|
+
const label = document.createElement("div");
|
|
134
|
+
label.className = "mcp-msg-label";
|
|
135
|
+
label.textContent = role === "user" ? "You" : "Assistant";
|
|
136
|
+
const body = document.createElement("div");
|
|
137
|
+
body.className = "mcp-msg-body";
|
|
138
|
+
body.innerHTML = bodyHtml;
|
|
139
|
+
wrap.appendChild(label);
|
|
140
|
+
wrap.appendChild(body);
|
|
141
|
+
thread.appendChild(wrap);
|
|
142
|
+
thread.scrollTop = thread.scrollHeight;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function appendToolNote(thread, toolName, args) {
|
|
146
|
+
const argsText = Object.keys(args).length ? JSON.stringify(args) : "{}";
|
|
147
|
+
appendMessage(
|
|
148
|
+
thread,
|
|
149
|
+
"user",
|
|
150
|
+
`<p class="mb-0"><code>${escapeHtml(toolName)}</code> <span class="text-muted">${escapeHtml(argsText)}</span></p>`,
|
|
151
|
+
false
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function appendResult(thread, text, isError) {
|
|
156
|
+
const formatted = escapeHtml(formatBody(text));
|
|
157
|
+
appendMessage(thread, "bot", `<pre class="mcp-result mb-0">${formatted}</pre>`, isError);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function callMcpTool(name, args) {
|
|
161
|
+
const headers = { "Content-Type": "application/json", Accept: "application/json" };
|
|
162
|
+
const apiKey = getApiKey();
|
|
163
|
+
if (apiKey) headers["X-API-Key"] = apiKey;
|
|
164
|
+
|
|
165
|
+
const res = await fetch("/docs/mcp/call", {
|
|
166
|
+
method: "POST",
|
|
167
|
+
credentials: "same-origin",
|
|
168
|
+
headers,
|
|
169
|
+
body: JSON.stringify({ name, arguments: args }),
|
|
170
|
+
});
|
|
171
|
+
const payload = await res.json().catch(() => ({ text: res.statusText, isError: true }));
|
|
172
|
+
if (!res.ok) {
|
|
173
|
+
const msg = payload.message || payload.text || res.statusText;
|
|
174
|
+
throw new Error(msg);
|
|
175
|
+
}
|
|
176
|
+
return payload;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function runTool(thread, name, args) {
|
|
180
|
+
appendToolNote(thread, name, args);
|
|
181
|
+
try {
|
|
182
|
+
const result = await callMcpTool(name, args);
|
|
183
|
+
appendResult(thread, result.text || "", !!result.isError);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
appendResult(thread, String(err.message || err), true);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function parseLimit(text) {
|
|
190
|
+
const match = text.match(/\blimit\s*=\s*(\d+)/i);
|
|
191
|
+
return match ? parseInt(match[1], 10) : 5;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function parseCommand(input) {
|
|
195
|
+
const text = input.trim();
|
|
196
|
+
if (!text) return null;
|
|
197
|
+
|
|
198
|
+
const lower = text.toLowerCase();
|
|
199
|
+
if (lower === "help" || lower === "?") {
|
|
200
|
+
return {
|
|
201
|
+
help: true,
|
|
202
|
+
html:
|
|
203
|
+
"<p>Commands:</p><ul class=\"mb-0\"><li><code>list models</code></li>" +
|
|
204
|
+
"<li><code>describe MODEL</code></li><li><code>count MODEL</code></li>" +
|
|
205
|
+
"<li><code>find MODEL</code> or <code>find MODEL limit=10</code></li></ul>",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (/^list\s+models?$/i.test(text)) {
|
|
210
|
+
return { tool: "jxp_list_models", args: {} };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const describeMatch = text.match(/^describe\s+(\S+)/i);
|
|
214
|
+
if (describeMatch) {
|
|
215
|
+
return { tool: "jxp_describe_model", args: { model: describeMatch[1] } };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const countMatch = text.match(/^count\s+(\S+)/i);
|
|
219
|
+
if (countMatch) {
|
|
220
|
+
return { tool: "jxp_count", args: { model: countMatch[1] } };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const findMatch = text.match(/^find\s+(\S+)/i);
|
|
224
|
+
if (findMatch) {
|
|
225
|
+
return {
|
|
226
|
+
tool: "jxp_find",
|
|
227
|
+
args: { model: findMatch[1], limit: parseLimit(text) },
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
error:
|
|
233
|
+
"I didn't understand that. Try <code>list models</code>, <code>describe MODEL</code>, " +
|
|
234
|
+
"<code>count MODEL</code>, or <code>find MODEL limit=5</code>. Type <code>help</code> for more.",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function handleUserInput(thread, input) {
|
|
239
|
+
const text = input.trim();
|
|
240
|
+
if (!text) return;
|
|
241
|
+
|
|
242
|
+
appendMessage(thread, "user", `<p class="mb-0">${escapeHtml(text)}</p>`, false);
|
|
243
|
+
|
|
244
|
+
const parsed = parseCommand(text);
|
|
245
|
+
if (!parsed) return;
|
|
246
|
+
|
|
247
|
+
if (parsed.help) {
|
|
248
|
+
appendMessage(thread, "bot", parsed.html, false);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (parsed.error) {
|
|
252
|
+
appendMessage(thread, "bot", `<p class="mb-0">${parsed.error}</p>`, true);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
await runTool(thread, parsed.tool, parsed.args);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function loadSessionApiKey() {
|
|
260
|
+
const access = document.documentElement.dataset.docsAccess;
|
|
261
|
+
if (access !== "protected") return;
|
|
262
|
+
try {
|
|
263
|
+
const res = await fetch("/docs/session", { credentials: "same-origin" });
|
|
264
|
+
if (!res.ok) return;
|
|
265
|
+
const data = await res.json();
|
|
266
|
+
if (!data.apikey) return;
|
|
267
|
+
const input = document.getElementById("docs-api-key");
|
|
268
|
+
if (input) input.value = data.apikey;
|
|
269
|
+
updateMcpConfigSnippets();
|
|
270
|
+
} catch {
|
|
271
|
+
/* ignore */
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
document.addEventListener("DOMContentLoaded", function () {
|
|
276
|
+
const access = document.documentElement.dataset.docsAccess;
|
|
277
|
+
const thread = document.getElementById("mcpChatThread");
|
|
278
|
+
const form = document.getElementById("mcpChatForm");
|
|
279
|
+
const chatInput = document.getElementById("mcpChatInput");
|
|
280
|
+
const toolSelect = document.getElementById("mcpToolSelect");
|
|
281
|
+
const toolArgs = document.getElementById("mcpToolArgs");
|
|
282
|
+
const toolRun = document.getElementById("mcpToolRun");
|
|
283
|
+
|
|
284
|
+
if (!thread) return;
|
|
285
|
+
|
|
286
|
+
loadSessionApiKey().then(function () {
|
|
287
|
+
if (access !== "protected") loadStoredKey();
|
|
288
|
+
updateMcpConfigSnippets();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const keyInput = document.getElementById("docs-api-key");
|
|
292
|
+
const remember = document.getElementById("docs-remember-key");
|
|
293
|
+
if (keyInput) {
|
|
294
|
+
keyInput.addEventListener("change", saveKeyIfRemembered);
|
|
295
|
+
keyInput.addEventListener("input", updateMcpConfigSnippets);
|
|
296
|
+
}
|
|
297
|
+
if (remember) remember.addEventListener("change", saveKeyIfRemembered);
|
|
298
|
+
|
|
299
|
+
document.querySelectorAll(".mcp-copy-btn").forEach(function (btn) {
|
|
300
|
+
btn.addEventListener("click", function () {
|
|
301
|
+
const target = btn.getAttribute("data-target");
|
|
302
|
+
if (target) copyMcpConfig(target, btn);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (form && chatInput) {
|
|
307
|
+
form.addEventListener("submit", function (e) {
|
|
308
|
+
e.preventDefault();
|
|
309
|
+
saveKeyIfRemembered();
|
|
310
|
+
const value = chatInput.value;
|
|
311
|
+
chatInput.value = "";
|
|
312
|
+
handleUserInput(thread, value);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
document.querySelectorAll(".mcp-quick-btn").forEach(function (btn) {
|
|
317
|
+
btn.addEventListener("click", function () {
|
|
318
|
+
saveKeyIfRemembered();
|
|
319
|
+
const cmd = btn.getAttribute("data-cmd") || "";
|
|
320
|
+
if (chatInput) chatInput.value = cmd;
|
|
321
|
+
handleUserInput(thread, cmd);
|
|
322
|
+
if (chatInput) chatInput.value = "";
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
if (toolRun && toolSelect && toolArgs) {
|
|
327
|
+
toolRun.addEventListener("click", function () {
|
|
328
|
+
saveKeyIfRemembered();
|
|
329
|
+
let args = {};
|
|
330
|
+
const raw = toolArgs.value.trim();
|
|
331
|
+
if (raw) {
|
|
332
|
+
try {
|
|
333
|
+
args = JSON.parse(raw);
|
|
334
|
+
} catch (err) {
|
|
335
|
+
appendResult(thread, "Invalid JSON arguments: " + err.message, true);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
runTool(thread, toolSelect.value, args);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
})();
|
package/templates/index.pug
CHANGED
|
@@ -11,6 +11,12 @@ block content
|
|
|
11
11
|
if first_guide_url
|
|
12
12
|
a.btn.btn-primary(href=first_guide_url) Get started
|
|
13
13
|
a.btn.btn-outline-primary(href="/docs/api") Browse API
|
|
14
|
+
if mcp_enabled
|
|
15
|
+
a.btn.btn-outline-primary(href="/docs/mcp")
|
|
16
|
+
i.bi.bi-robot.me-1
|
|
17
|
+
| Ask MCP
|
|
18
|
+
else
|
|
19
|
+
a.btn.btn-outline-secondary(href="/docs/md/mcp.md") MCP setup
|
|
14
20
|
a.btn.btn-outline-secondary(href="/docs/diagnostics") Index diagnostics
|
|
15
21
|
a.btn.btn-outline-secondary(href="/docs/md/api.md") REST reference
|
|
16
22
|
|
|
@@ -35,6 +41,37 @@ block content
|
|
|
35
41
|
| panels on API resource pages. Keys are stored in the browser only when “Remember” is checked.
|
|
36
42
|
.col-md-6
|
|
37
43
|
.card.h-100
|
|
44
|
+
.card-header.fw-semibold
|
|
45
|
+
if mcp_enabled
|
|
46
|
+
| MCP for AI agents
|
|
47
|
+
else
|
|
48
|
+
| MCP (Model Context Protocol)
|
|
49
|
+
.card-body
|
|
50
|
+
if mcp_enabled
|
|
51
|
+
p.mb-2
|
|
52
|
+
| This server exposes read-only MCP tools at
|
|
53
|
+
code= mcp_path
|
|
54
|
+
| — use them from Cursor, LM Studio, or the built-in assistant.
|
|
55
|
+
pre.mb-3
|
|
56
|
+
code.
|
|
57
|
+
MCP_ENABLED=true
|
|
58
|
+
# Cursor: npx -y jxp-mcp
|
|
59
|
+
a.btn.btn-sm.btn-primary(href="/docs/mcp") Open MCP assistant
|
|
60
|
+
a.btn.btn-sm.btn-outline-secondary.ms-2(href="/docs/md/mcp.md") Setup guide
|
|
61
|
+
else
|
|
62
|
+
p.mb-2
|
|
63
|
+
| Enable
|
|
64
|
+
code MCP_ENABLED=true
|
|
65
|
+
| to expose five fixed read-only tools (
|
|
66
|
+
code jxp_list_models
|
|
67
|
+
| ,
|
|
68
|
+
code jxp_find
|
|
69
|
+
| , …) for AI clients and a built-in docs playground.
|
|
70
|
+
a.btn.btn-sm.btn-outline-primary(href="/docs/md/mcp.md") Read MCP docs
|
|
71
|
+
|
|
72
|
+
.row.g-4.mb-4
|
|
73
|
+
.col-12
|
|
74
|
+
.card
|
|
38
75
|
.card-header.fw-semibold API at a glance
|
|
39
76
|
.card-body
|
|
40
77
|
p.mb-2
|