@bloklabs/mcp 0.0.0-stage → 2.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/README.md +267 -2
- package/bin/blok-mcp.mjs +9 -0
- package/dist/mcp.mjs +60279 -0
- package/dist/stdout-guard.mjs +10 -0
- package/package.json +49 -4
- package/types/index.d.ts +103 -0
package/README.md
CHANGED
|
@@ -1,3 +1,268 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @bloklabs/mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An MCP server for [Blok](https://blokeditor.com). It lets outside agents read and edit Blok documents.
|
|
4
|
+
|
|
5
|
+
## What it is
|
|
6
|
+
|
|
7
|
+
The server exposes six tools.
|
|
8
|
+
|
|
9
|
+
| Tool | What it does |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `blok_list_documents` | Lists documents the caller may open, if the source can list them. |
|
|
12
|
+
| `blok_open` | Opens a document and returns a `handle`. Every other tool takes it. Without `documentId` it opens the only document, or the one whose id or title equals `query`; otherwise it returns the list to choose from. |
|
|
13
|
+
| `blok_read` | Returns the document view for a handle. |
|
|
14
|
+
| `blok_describe` | Returns the command contract, or one tool or command. |
|
|
15
|
+
| `blok_execute` | Runs one batch of commands on a handle. |
|
|
16
|
+
| `blok_close` | Closes a handle. Live: leaves the room. Stored: optional, since every batch already saved. |
|
|
17
|
+
|
|
18
|
+
A document opens in one of two modes:
|
|
19
|
+
|
|
20
|
+
- **Live**: the agent joins the document's sync room as a participant. People see it work.
|
|
21
|
+
- **Stored**: the agent loads the saved document, edits it, and saves it back after every batch.
|
|
22
|
+
|
|
23
|
+
Agents write rich text with commands. They never write Markdown or HTML into block data. On initialize the server sends `instructions`:
|
|
24
|
+
|
|
25
|
+
- The rich-text rules: the marks, colour presets, link defaults, and equation and page embeds.
|
|
26
|
+
- The editing rules the in-app assistant gets too, such as "change only the blocks the request is about" and "put related edits in one `blok_execute` call".
|
|
27
|
+
- How to reach a sub-page: open the page block's `data.pageId` with `blok_open`.
|
|
28
|
+
- How to make a sub-page, where the server can: insert a page block. See `--page-titles page-map`.
|
|
29
|
+
|
|
30
|
+
## Prompts
|
|
31
|
+
|
|
32
|
+
The server also offers the built-in Ask AI buttons as MCP prompts. A client lists them with `prompts/list` and shows them, for example as slash commands. The prompt text is the one Ask AI sends.
|
|
33
|
+
|
|
34
|
+
| Prompt | What it does |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `improve` | Improve writing |
|
|
37
|
+
| `proofread` | Fix spelling and grammar |
|
|
38
|
+
| `shorter` | Make shorter |
|
|
39
|
+
| `longer` | Make longer |
|
|
40
|
+
| `simplify` | Simplify language |
|
|
41
|
+
| `explain` | Explain |
|
|
42
|
+
| `summarize` | Summarize |
|
|
43
|
+
| `actionItems` | Find action items |
|
|
44
|
+
| `continue` | Continue writing |
|
|
45
|
+
|
|
46
|
+
The arguments:
|
|
47
|
+
|
|
48
|
+
- `documentId`: the document to work on. Without it, `blok_open` lists the documents to choose from.
|
|
49
|
+
- `selection`: the selected text, word for word. `improve`, `shorter`, `longer`, `simplify` and `explain` need it. With it, `proofread` fixes only that text; without it, the whole document.
|
|
50
|
+
- `blockId`: the block the selection is in, or, for `continue`, the block to write after.
|
|
51
|
+
|
|
52
|
+
The prompt tells the model to open the document with `blok_open`, find the text with `doc.find` and edit with `blok_execute`. Host buttons set in `agent.assistant.actions` are not served.
|
|
53
|
+
|
|
54
|
+
## Install and run locally
|
|
55
|
+
|
|
56
|
+
Run it against a folder of JSON documents:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
npx @bloklabs/mcp --source files:./docs
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
It speaks MCP over stdio by default. Add it to Claude Code or Claude Desktop like this:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"mcpServers": {
|
|
67
|
+
"blok": {
|
|
68
|
+
"command": "npx",
|
|
69
|
+
"args": ["@bloklabs/mcp", "--source", "files:/abs/path/to/docs"]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Over stdio, credentials come from environment variables, not from the MCP client.
|
|
76
|
+
|
|
77
|
+
## Live rooms
|
|
78
|
+
|
|
79
|
+
Pass `--sync-url` to join rooms on a Blok sync service.
|
|
80
|
+
|
|
81
|
+
- `--origin` sets the `Origin` header the server sends to the room.
|
|
82
|
+
- A sync service in ticket mode checks `Origin` on every client, browser or not.
|
|
83
|
+
- So a backend must send an origin the service allows (its `--allow-origin` list).
|
|
84
|
+
- `Origin` is a cross-site guard, not proof of identity. The ticket is what authenticates.
|
|
85
|
+
- Tickets come from one of two places:
|
|
86
|
+
- `BLOK_SECRET`: the MCP server signs tickets itself with the sync service's secret.
|
|
87
|
+
- `--ticket-url` with `BLOK_MCP_HOST_TOKEN`: the MCP server asks your host for a ticket. The host checks the human's rights. This is the recommended hosted mode.
|
|
88
|
+
- `--sync-url` needs one of the two. Without a ticket the room cannot name the agent, so the server refuses to start.
|
|
89
|
+
- `BLOK_SECRET` over `--http` also needs `--authorize`. Otherwise any caller could get a ticket for any document. Over stdio it is allowed alone.
|
|
90
|
+
- The ticket's `user` must equal the agent's actor id, not the human's id. Otherwise the open is refused.
|
|
91
|
+
- The ticket route answers 403 when the human may not open the document. The agent gets `FORBIDDEN`, and a live handle ends. A 401 means the host token was refused; the agent gets a retryable error.
|
|
92
|
+
- `blok_open` reports the handle's `durability`:
|
|
93
|
+
- `acknowledged`: the server has a journal, and each batch waits for its acks.
|
|
94
|
+
- `best-effort`: the server has no journal and makes no durability claim.
|
|
95
|
+
- `saved-on-execute`: a stored document, saved after every batch.
|
|
96
|
+
- `blok_open` lists the commands a live handle refuses in `unavailableCommands`, each with a `reason`. `blok_describe` marks them `available: false` too.
|
|
97
|
+
- `history.undo` and `history.redo` take back and redo the handle's own batches. They never touch what other people wrote. When someone else changed what a batch added, removed or moved, they answer `CONFLICT` and change nothing.
|
|
98
|
+
- Each `blok_execute` result says what happened to that batch in `delivery`.
|
|
99
|
+
- `blok_close` reports `hostRecordLags`. The host's stored record lags the agent's edits until the room writes it back.
|
|
100
|
+
|
|
101
|
+
## Stored documents
|
|
102
|
+
|
|
103
|
+
Pick a source with `--source`.
|
|
104
|
+
|
|
105
|
+
**`--source endpoint:<url>`** reuses the document endpoint contract the sync service already uses.
|
|
106
|
+
|
|
107
|
+
- Load: `GET <url>/<doc>`. Save: `PUT <url>/<doc>` with `Blok-Doc-Version`.
|
|
108
|
+
- A document the host never saved (`null`, or `{"data": null}`) opens empty, and `blok_open` reports `exists: false`. So does a missing file under `files:`. The first save creates it.
|
|
109
|
+
- `BLOK_DOC_ENDPOINT_AUTH` is sent as the `Authorization` header. It opens every document, so run this source only from a trusted backend.
|
|
110
|
+
- Answer a stale `PUT` with `409`. Without it, saves are last-writer-wins.
|
|
111
|
+
- `--source-list <url>` turns on `blok_list_documents`. The route contract:
|
|
112
|
+
- Request: `GET <url>?query=&cursor=&limit=`.
|
|
113
|
+
- Response: `{ "documents": [{ "id": "...", "title": "..." }], "nextCursor": "..." }`. `title` and `nextCursor` are optional.
|
|
114
|
+
|
|
115
|
+
**`--source files:<dir>`** stores each document as `<dir>/<id>.json`. It is meant for local use.
|
|
116
|
+
|
|
117
|
+
- An id uses letters, digits, `.`, `_` and `-`, does not start with `.`, and is at most 200 characters.
|
|
118
|
+
- Ids are case-sensitive. An id that matches a stored file only when case is ignored is refused.
|
|
119
|
+
- On a disk that ignores case (the macOS and Windows default), two ids that differ only by case cannot both exist. Whichever is saved first owns the name.
|
|
120
|
+
- Saves are atomic: a temp file, then a rename. The file keeps its permissions, and the data is flushed to disk before the rename.
|
|
121
|
+
- A save checks a SHA-256 hash of the file first. If the file changed since it was opened, the save is refused.
|
|
122
|
+
- Saves of one file run one at a time, across processes too. Each save holds a lock file beside the file it writes, named after it in lower case: `.<file name>.lock`, like `.doc.json.lock`.
|
|
123
|
+
- Only blok-mcp honors the lock. Another program writing the file is still caught by the hash check, except in the moment between the check and the rename.
|
|
124
|
+
- A lock older than 30 seconds is treated as left by a crashed process and removed.
|
|
125
|
+
- A document that is a symlink is saved through to its target, so the link stays. The lock sits beside the target, so saves through the link and saves of the target wait for each other.
|
|
126
|
+
- A link to a missing file is refused, on open and on save.
|
|
127
|
+
- `page.rename` and `page.setIcon` refuse a page block whose id links to the open document, as they do for the open document's own id.
|
|
128
|
+
|
|
129
|
+
Stored mode is refused when `--sync-url` is set. A sync service owns those documents, and a stored write would fight the room.
|
|
130
|
+
|
|
131
|
+
## Hosted
|
|
132
|
+
|
|
133
|
+
- `--http <host:port>` serves Streamable HTTP instead of stdio.
|
|
134
|
+
- A non-loopback address needs `--public`. `--public` needs `--auth oauth`.
|
|
135
|
+
- Over HTTP the server also needs a per-document check, or it refuses to start. OAuth says who is calling, not which documents they may open.
|
|
136
|
+
- Pass `--authorize`, or
|
|
137
|
+
- serve live rooms only: `--ticket-url` with no `--source`, so your host decides each ticket.
|
|
138
|
+
- `--auth oauth` makes the server an OAuth resource server. It needs `--oauth-issuer`, `--oauth-audience` and `--oauth-jwks`.
|
|
139
|
+
- Tokens are checked against the issuer's JWKS. The audience must name this server.
|
|
140
|
+
- Tokens must be JWT access tokens (RFC 9068): the header `typ` must be `at+jwt` or `application/at+jwt`. Tokens with any other `typ`, or none, are refused, so an ID token cannot pass as an access token.
|
|
141
|
+
- Scopes: `blok.read` (list, open read-only, read, describe) and `blok.write` (open for writing, execute).
|
|
142
|
+
- Protected Resource Metadata is served at `/.well-known/oauth-protected-resource`.
|
|
143
|
+
- The principal is the token's `sub`. A token with no `sub` gets `client:<client_id>` (or its `azp`), so a machine client never shares a user's handles or `--authorize` answers.
|
|
144
|
+
- A `sub` that is not a non-empty string, or that starts with `client:`, is refused. So is a token that names no `sub`, `client_id` or `azp`.
|
|
145
|
+
- A request with a non-loopback `Origin` header is refused.
|
|
146
|
+
- Live handles hold sockets, so run it as a long-lived process.
|
|
147
|
+
- To scale out, route on the `Mcp-Param-Blok-Handle` header, so every call for a handle reaches the process that holds it.
|
|
148
|
+
- Some clients do not mirror `x-mcp-header` parameters into headers. To serve them, run a single instance.
|
|
149
|
+
|
|
150
|
+
## Host duties
|
|
151
|
+
|
|
152
|
+
- On an ASP.NET host, `IBlokAuthorization` sees the agent's actor id, not the human's. Map it back to the human for your document checks.
|
|
153
|
+
- `--authorize <module|url>` decides who may open what:
|
|
154
|
+
- A URL is a webhook. It gets `POST { "principal", "documentId", "write" }` and must answer `200 { "allowed": true | false }`. Any other answer means no.
|
|
155
|
+
- Anything else is a module path. Its default export is `(principal, documentId, write) => Promise<boolean>`.
|
|
156
|
+
- It runs before every open, and again on each later call.
|
|
157
|
+
- `blok_list_documents` shows only the documents it allows the caller to read. Without it, the list is unfiltered.
|
|
158
|
+
- `--page-titles page-map` lets agents rename other pages and set their icons. It shows up only if your host reads each page's title from that page document's title.
|
|
159
|
+
- It also lets agents make sub-pages. Inserting a page block creates a document under a new id, and the block's `data.pageId` names it. The agent then names it with `page.rename` and fills it with `blok_open`.
|
|
160
|
+
- The new id must pass `--authorize` as a write first. If it does not, nothing is created and the insert fails with `FORBIDDEN`.
|
|
161
|
+
- In stored mode the server saves an empty document there.
|
|
162
|
+
- With `--sync-url` the server joins the new page's room once and leaves. The sync service loads it empty and seeds it, so its ticket and `IBlokAuthorization` must let the agent open an id it has never seen, and your document endpoint must answer `null` for it. If the room is refused, the insert fails with `FORBIDDEN` or `SOURCE_UNAVAILABLE`.
|
|
163
|
+
- In live mode your document endpoint gets no PUT for the new page yet. It gets one when someone writes in the page, or under a journal when the room is opened again.
|
|
164
|
+
- The document stays, empty, if the batch fails after it was made or the insert is undone. When a later command in that batch fails, `error.details.orphaned` lists it. A save or a room that refuses the batch does not. Delete it yourself if you do not want it.
|
|
165
|
+
- Without `page-map`, a page insert fails with `COMMAND_UNAVAILABLE`, reason `host`. The page entry in `blok_describe` says the same: `insertUnavailableReason: "host"`.
|
|
166
|
+
- One batch inserts at most 25 pages. A longer batch fails with `INVALID_ARGS` before any page is made.
|
|
167
|
+
- Insert each page with its own `block.insert`. A page inside another block's `children` fails with `INVALID_ARGS`. A `block.update` that changes `data.pageId` fails with `FIELD_NOT_WRITABLE`.
|
|
168
|
+
- `block.delete` on a page block removes only the link, as a person's Move to Trash does. The page document is never opened or changed, so trash and clean-up stay with your host. Undo brings back the same `pageId`, and redo makes no new page.
|
|
169
|
+
- An agent never deletes a database a person locked, or a block that holds one. That delete fails with `PRECONDITION_FAILED`, reason `locked`. A person can still delete it in the editor.
|
|
170
|
+
|
|
171
|
+
## Uploads
|
|
172
|
+
|
|
173
|
+
An agent can upload a file with `image.uploadFile`, `video.uploadFile`, `audio.uploadFile` or `file.uploadFile`. Each inserts a new block, the same block a person's upload gives.
|
|
174
|
+
|
|
175
|
+
- Only when you embed the server and pass `uploads` to `createBlokMcpServer`. There is no flag for it. Without it, these commands answer `COMMAND_UNAVAILABLE` with reason `host`.
|
|
176
|
+
- `uploads.uploader.uploadByFile(file, { kind, tool })` is your own uploader. It gets a `File` and returns `{ url, fileName? }`. A file card also keeps `size` and `mimeType` when you return them.
|
|
177
|
+
- The agent sends the bytes as base64 `data` with a `fileName`. Over HTTP the request body is capped at 4 MiB, so large files need stdio.
|
|
178
|
+
- `uploads.root` lets the agent send a `path` instead. The server reads only regular files under that folder.
|
|
179
|
+
- A path that leaves the folder, through `..`, an absolute path or a symlink, is refused.
|
|
180
|
+
- FIFOs, devices and folders are refused.
|
|
181
|
+
- It works over stdio only. Over HTTP the caller is remote, so the server refuses to start with it.
|
|
182
|
+
- The type comes from the file name's extension, as a browser sets it. A `mimeType` that disagrees is refused.
|
|
183
|
+
- `uploads.limits` sets `types` and `maxSize` per kind, like a media tool's own config. The defaults are the tools' defaults: `image/*`, `audio/*` and `video/*`, any type for files; 30 MiB, and 100 MiB for video.
|
|
184
|
+
- The size is checked before the bytes are decoded.
|
|
185
|
+
- An SVG is sent unchanged, as a person's upload sends it. Leave `image/svg+xml` out of `types` if your storage serves it on your own origin.
|
|
186
|
+
- An upload runs alone in its batch. When the batch fails after the upload, `error.details.orphaned` names the stored file. Delete it yourself.
|
|
187
|
+
- The server does not compress images, turn GIFs into video, read audio tags or make variants. The editor does those in the browser.
|
|
188
|
+
|
|
189
|
+
## Locked databases
|
|
190
|
+
|
|
191
|
+
An agent unlocks a database a person locked only after a person says yes.
|
|
192
|
+
|
|
193
|
+
- If your client supports MCP elicitation, the server asks there. The person sees which database it is, and only ticking Unlock lets the agent go on. If they say no, nothing in that call is applied.
|
|
194
|
+
- If your client does not support elicitation, the unlock fails with `APPROVAL_REQUIRED`. The person can unlock it in the editor.
|
|
195
|
+
- Blok trusts the client to show the question to a person. The model cannot answer it through tool arguments.
|
|
196
|
+
|
|
197
|
+
## Custom tools
|
|
198
|
+
|
|
199
|
+
- `--manifest <file>` loads your custom block and inline tools, as a `BlokCustomToolsFile`.
|
|
200
|
+
- List each custom inline tool under `inlineTools`. Give its `name`, an optional `title`, and its `sanitize` rules.
|
|
201
|
+
- Copy `sanitize` from the tool's `static sanitize`. Each tag maps to `true` or to its attributes, like `{"kbd":{"data-key":true}}`.
|
|
202
|
+
- Function rules cannot go in a JSON file. Write the attributes out instead.
|
|
203
|
+
- Its marks are `tag:<tag>` keys. Agents see them in the schema, and `blok_describe` answers for the tool by name.
|
|
204
|
+
- Without this entry, the server removes the tool's marks when it writes the block.
|
|
205
|
+
- The tool applies to every built-in block but code, and to your custom blocks. The file cannot limit it to some blocks.
|
|
206
|
+
- Only text-formatting tags are allowed: `kbd`, `abbr`, `cite`, `dfn`, `q`, `small`, `samp`, `var`, `time`, `data`, `span`, `mark`, `code`, `a`, `ruby`, `rt`, `rp`, `bdi`, `bdo`, `ins`, `del`, `s`, `u`, `b`, `i`, `strong`, `em`, `sup`, `sub`.
|
|
207
|
+
- Only plain attributes are allowed: `class`, `id`, `title`, `lang`, `dir`, `role`, `translate`, `datetime`, `value`, `data-*` and `aria-*`. A link (`a`) may also have `href`, `target` and `rel`. Its `href` gets the same unsafe-URL check as Blok's own links.
|
|
208
|
+
- Anything else is refused, because the server cannot check it for unsafe URLs. That includes `style`, `on*`, `cite`, `xlink:href` and `data-blok-*`.
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"formatVersion": 1,
|
|
213
|
+
"blocks": [],
|
|
214
|
+
"inlineTools": [{ "name": "kbd", "title": "Keyboard", "sanitize": { "kbd": { "data-key": true } } }]
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
- `--manifest-overrides <file>` hides tools or actions, or adds guidance.
|
|
219
|
+
- `--rich-text-fields` (or `BLOK_RICH_TEXT_FIELDS`) names rich text fields as JSON, like `{"callout":["title"]}`. It is used only for block types missing from the manifest.
|
|
220
|
+
- Custom tool action handlers do not run here. Custom tools get structural commands and field writes.
|
|
221
|
+
|
|
222
|
+
## All flags
|
|
223
|
+
|
|
224
|
+
A flag wins over its environment variable.
|
|
225
|
+
|
|
226
|
+
Pass a value as the next argument: `--auth oauth`, not `--auth=oauth`. An unknown flag or a stray argument is refused at startup.
|
|
227
|
+
|
|
228
|
+
| Flag | Env var | Default | What it does |
|
|
229
|
+
|---|---|---|---|
|
|
230
|
+
| `--source files:<dir>` / `endpoint:<url>` | | none | Where stored documents live. |
|
|
231
|
+
| `--source-list <url>` | | none | List route for the `endpoint` source. |
|
|
232
|
+
| | `BLOK_DOC_ENDPOINT_AUTH` | empty | `Authorization` header for the `endpoint` source. |
|
|
233
|
+
| `--sync-url <url>` | `BLOK_SYNC_URL` | none | Sync service for live rooms. |
|
|
234
|
+
| `--origin <origin>` | `BLOK_MCP_ORIGIN` | none | `Origin` sent to the sync service. |
|
|
235
|
+
| | `BLOK_SECRET` | none | Signs room tickets locally. |
|
|
236
|
+
| `--ticket-url <url>` | `BLOK_TICKET_URL` | none | Host route that mints room tickets. |
|
|
237
|
+
| | `BLOK_MCP_HOST_TOKEN` | none | Bearer token for `--ticket-url`. Required with it. |
|
|
238
|
+
| `--http <host:port>` | `BLOK_MCP_HTTP` | stdio | Serve Streamable HTTP. |
|
|
239
|
+
| `--public` | | off | Allow a non-loopback `--http` address. |
|
|
240
|
+
| `--auth none\|oauth` | | `none` | How HTTP callers authenticate. |
|
|
241
|
+
| `--oauth-issuer <url>` | | none | Expected token issuer. |
|
|
242
|
+
| `--oauth-audience <url>` | | none | This server's resource id. |
|
|
243
|
+
| `--oauth-jwks <url>` | | none | Issuer's JWKS URL. |
|
|
244
|
+
| `--authorize <module\|url>` | | none | Per-document access hook. |
|
|
245
|
+
| `--principal <name>` | | `local` | Who the caller is over stdio. |
|
|
246
|
+
| `--manifest <file>` | | none | Custom block and inline tools. |
|
|
247
|
+
| `--manifest-overrides <file>` | | none | Tool and action overrides. |
|
|
248
|
+
| `--rich-text-fields <json>` | `BLOK_RICH_TEXT_FIELDS` | none | Rich text fields for unknown block types. |
|
|
249
|
+
| `--page-titles off\|page-map` | | `off` | Headless page rename and icon, and new sub-pages. |
|
|
250
|
+
| `--schema mixed\|envelope\|full` | | `mixed` | How `blok_execute` describes its commands. `mixed` gives the full arguments of the common commands, so the model needs no `blok_describe` call for them. |
|
|
251
|
+
| `--handle-idle-ms <ms>` | | `600000` | Idle time before a handle expires. |
|
|
252
|
+
| `--max-handles <n>` | | `8` | Open handles per principal. |
|
|
253
|
+
| `--no-comments` | | comments on | Turn off the database comment commands. A host that keeps comments in its own system (a `comments` hook) must pass it, or agent comments stay invisible to people. `--comments` is accepted and changes nothing. |
|
|
254
|
+
|
|
255
|
+
## Limits
|
|
256
|
+
|
|
257
|
+
- Uploads need an embedded server with `uploads` (see Uploads). The CLI adds media by URL only.
|
|
258
|
+
- No Docker image.
|
|
259
|
+
- A handle expires after 10 minutes idle.
|
|
260
|
+
- A principal may hold 8 open handles.
|
|
261
|
+
|
|
262
|
+
## Versions
|
|
263
|
+
|
|
264
|
+
Upgrade `@bloklabs/core`, `@bloklabs/server` and `@bloklabs/mcp` together. They share the room format. A room in a format this server does not know fails with `VERSION_SKEW`.
|
|
265
|
+
|
|
266
|
+
## Embedding
|
|
267
|
+
|
|
268
|
+
`createBlokMcpServer(options)` runs the same server inside your own Node backend. Its options are typed as `BlokMcpOptions`.
|
package/bin/blok-mcp.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Guard before importing the CLI; module-load logs would corrupt stdout.
|
|
3
|
+
import { installStdoutGuard } from '../dist/stdout-guard.mjs';
|
|
4
|
+
|
|
5
|
+
installStdoutGuard(console);
|
|
6
|
+
|
|
7
|
+
const { runCli } = await import('../dist/mcp.mjs');
|
|
8
|
+
|
|
9
|
+
await runCli(process.argv.slice(2), process.env);
|