@behavioralstate/best-mcp 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 +296 -0
- package/dist/index.js +854 -0
- package/package.json +22 -0
- package/src/index.ts +921 -0
- package/tsconfig.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# best-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for any [BEST-compliant](https://behavioralstate.io) endpoint. Exposes the BEST command and query surface as MCP tools so any LLM client (ChatGPT Desktop, Claude Desktop, GitHub Copilot, Cursor) can discover and interact with a BEST service.
|
|
4
|
+
|
|
5
|
+
Supports **multiple named connections** in a single server instance — useful for admins who need to operate across tenant-scoped and platform-level surfaces, or across entirely separate BEST applications.
|
|
6
|
+
|
|
7
|
+
## Who this is for
|
|
8
|
+
|
|
9
|
+
best-mcp is an **adapter for clients you don't control**. If you use an off-the-shelf MCP-capable client, this server is the right integration: it is the only plug-in mechanism those clients offer.
|
|
10
|
+
|
|
11
|
+
If you are writing **your own** agent, backend, or tooling, you don't need it — call the BEST HTTP surface directly. BEST endpoints are self-describing (command/query catalogues, JSON Schemas, workflows), and every tool below is a thin wrapper over exactly one HTTP call. Putting best-mcp between your own code and the service adds a network hop and a deployment to operate, flattens structured BEST error responses into prose, and widens your supply chain — while providing nothing a small HTTP client in your codebase wouldn't. See [Choosing a Transport](https://behavioralstate.io/docs/transports/mcp) in the spec docs.
|
|
12
|
+
|
|
13
|
+
Running it as a shared server in production? **Pin a version** (`npx @behavioralstate/best-mcp@1.7.0`, or your package manager's equivalent) rather than resolving `latest` at start-up — callers' credentials flow through this process, so upgrades should be deliberate.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Start with AI
|
|
18
|
+
|
|
19
|
+
Paste either prompt into your LLM client to get configured in under a minute.
|
|
20
|
+
|
|
21
|
+
**Configure best-mcp** — generates the exact env vars and `mcpServers` JSON for your client:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
Configure best-mcp so I can use my BEST service from [VS Code Copilot / Claude Desktop / Cursor].
|
|
25
|
+
|
|
26
|
+
Service base URL: [https://api.example.com/best]
|
|
27
|
+
API key: [my-api-key]
|
|
28
|
+
Tenant ID: [my-tenant-id] ← remove this line if not multi-tenant
|
|
29
|
+
|
|
30
|
+
Output the exact env vars and mcpServers JSON block to add to my client config.
|
|
31
|
+
|
|
32
|
+
best-mcp docs: https://behavioralstate.io/docs/transports/mcp
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Make your service BEST-compliant** — scaffolds the four required endpoints in your framework:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
Make my [ASP.NET Core / Express / FastAPI / Spring Boot] service BEST-compliant.
|
|
39
|
+
|
|
40
|
+
I need these four endpoints:
|
|
41
|
+
- GET /.well-known/best — discovery manifest
|
|
42
|
+
- GET /commands — catalogue listing accepted commands with JSON Schema
|
|
43
|
+
- POST /commands — CloudEvents 1.0 entry point
|
|
44
|
+
- GET /queries — query catalogue
|
|
45
|
+
|
|
46
|
+
Auth: X-Api-Key header. Set authentication.type = "apikey" in the manifest.
|
|
47
|
+
|
|
48
|
+
Spec reference: https://behavioralstate.io/docs
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Tools
|
|
54
|
+
|
|
55
|
+
| Tool | What it does |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `list_connections` | List all configured connections with names, endpoints, and descriptions *(only shown when multiple connections are configured)* |
|
|
58
|
+
| `get_command_catalogue` | List all commands this endpoint accepts |
|
|
59
|
+
| `get_command_schema` | Fetch the full JSON Schema for a command type — learn the exact fields required |
|
|
60
|
+
| `send_command` | Send a command (CloudEvent 1.0 envelope built automatically) |
|
|
61
|
+
| `send_command_and_wait` | Send a command then poll a query until a condition is met |
|
|
62
|
+
| `get_query_catalogue` | List all read queries this endpoint exposes |
|
|
63
|
+
| `get_query_schema` | Fetch the JSON Schema for a query — learn parameters and response shape |
|
|
64
|
+
| `execute_query` | Execute a query and return current state synchronously |
|
|
65
|
+
| `get_workflows` | List the service's published "descriptive sequence" recipes — an optional vendor extension; returns a note if the service publishes none |
|
|
66
|
+
|
|
67
|
+
Intended LLM flow: `get_command_catalogue` → pick a command → `get_command_schema` → gather fields → `send_command`.
|
|
68
|
+
Optionally call `get_workflows` first to see if the service publishes a ready-made recipe for a multi-step process.
|
|
69
|
+
|
|
70
|
+
When multiple connections are configured all operation tools gain an optional `connection` parameter. If the LLM is not certain which connection the user intends, it calls `list_connections` and asks the user to confirm before proceeding.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Setup
|
|
75
|
+
|
|
76
|
+
### 1. Install and build
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
cd mcp-server
|
|
80
|
+
npm install
|
|
81
|
+
npm run build
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 2. Configure
|
|
85
|
+
|
|
86
|
+
There are three configuration modes. Use whichever fits your setup — they are mutually exclusive and checked in the order listed.
|
|
87
|
+
|
|
88
|
+
> **Upgrading from `bsp-mcp` (pre-2.0)?** The protocol short name changed BSP → BEST in spec 0.9.0. All env vars are now `BEST_*`, but the server accepts legacy `BSP_*` names as a deprecated fallback (with a startup warning), so existing configurations keep working — rename them at your convenience.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
### Mode 1 — Per-app env vars *(recommended)*
|
|
93
|
+
|
|
94
|
+
One set of `BEST_<APP>_*` variables per application. The app name is a single uppercase word (letters and digits, no underscores), e.g. `TRADING`, `HR`, `ACCOUNTING`.
|
|
95
|
+
|
|
96
|
+
#### Required
|
|
97
|
+
|
|
98
|
+
| Variable | Description |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `BEST_<APP>_BASE_URL` | Root URL of the BEST HTTP surface |
|
|
101
|
+
| `BEST_<APP>_API_KEY` | Credential — not required when `AUTH_TYPE=none` |
|
|
102
|
+
|
|
103
|
+
#### Optional
|
|
104
|
+
|
|
105
|
+
| Variable | Default | Description |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `BEST_<APP>_TENANT_ID` | — | When set, **auto-generates two connections**: `<app>/tenant` (tenant-scoped) and `<app>/platform` (platform-level). When omitted, generates one connection: `<app>`. |
|
|
108
|
+
| `BEST_<APP>_AUTH_TYPE` | `apikey` | How the credential is sent — see [Auth types](#auth-types) below. **Defaults to `apikey` in Mode 1** (unlike Modes 2 and 3 which default to `bearer`). |
|
|
109
|
+
| `BEST_<APP>_AUTH_HEADER` | `X-Api-Key` | Header name — only used when `AUTH_TYPE=apikey` and `AUTH_IN=header` |
|
|
110
|
+
| `BEST_<APP>_AUTH_IN` | `header` | Where the key is sent when `AUTH_TYPE=apikey`: `header` or `query` |
|
|
111
|
+
| `BEST_<APP>_AUTH_PARAM` | `apikey` | Query parameter name — only used when `AUTH_IN=query` |
|
|
112
|
+
| `BEST_<APP>_ALLOW_BEARER_PASSTHROUGH` | `false` | Allow a per-request `Authorization: Bearer <token>` header to be forwarded to the BEST endpoint as the caller's own credential — see [Per-request credential overrides](#http--per-request-credential-overrides-multi-user-backends). |
|
|
113
|
+
|
|
114
|
+
#### Auth types
|
|
115
|
+
|
|
116
|
+
> **Default differs by mode.** Mode 1 defaults to `apikey` because BEST services typically use API key headers. Modes 2 and 3 default to `bearer` for backward compatibility.
|
|
117
|
+
|
|
118
|
+
| `AUTH_TYPE` | What it does | Extra vars needed |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `apikey` *(Mode 1 default)* | Sends the key in a custom header or query param | `AUTH_HEADER` (header name, default `X-Api-Key`) or `AUTH_IN=query` + `AUTH_PARAM` |
|
|
121
|
+
| `bearer` *(Modes 2 & 3 default)* | Sends `Authorization: Bearer <key>` | none |
|
|
122
|
+
| `none` | No credentials sent (public endpoint) | `API_KEY` not required |
|
|
123
|
+
|
|
124
|
+
#### Examples
|
|
125
|
+
|
|
126
|
+
**Single app, tenant + platform surfaces (most common admin setup):**
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
BEST_TRADING_BASE_URL=https://api.example.com/best
|
|
130
|
+
BEST_TRADING_API_KEY=your-api-key
|
|
131
|
+
BEST_TRADING_TENANT_ID=your-tenant-id
|
|
132
|
+
BEST_TRADING_AUTH_TYPE=apikey
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
This generates two connections automatically:
|
|
136
|
+
- `trading/tenant` → `https://api.example.com/best/tenants/your-tenant-id`
|
|
137
|
+
- `trading/platform` → `https://api.example.com/best`
|
|
138
|
+
|
|
139
|
+
**Two separate apps:**
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
BEST_TRADING_BASE_URL=https://trading.example.com/best
|
|
143
|
+
BEST_TRADING_API_KEY=trading-key
|
|
144
|
+
BEST_TRADING_TENANT_ID=tenant-abc
|
|
145
|
+
BEST_TRADING_AUTH_TYPE=apikey
|
|
146
|
+
|
|
147
|
+
BEST_HR_BASE_URL=https://hr.example.com/best
|
|
148
|
+
BEST_HR_API_KEY=hr-key
|
|
149
|
+
BEST_HR_TENANT_ID=tenant-abc
|
|
150
|
+
BEST_HR_AUTH_TYPE=apikey
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
This generates four connections: `trading/tenant`, `trading/platform`, `hr/tenant`, `hr/platform`.
|
|
154
|
+
|
|
155
|
+
**App with no tenant scope:**
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
BEST_MYAPP_BASE_URL=https://api.example.com/best
|
|
159
|
+
BEST_MYAPP_API_KEY=your-api-key
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Generates one connection: `myapp`.
|
|
163
|
+
|
|
164
|
+
#### MCP client config (stdio)
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"mcpServers": {
|
|
169
|
+
"best": {
|
|
170
|
+
"command": "npx",
|
|
171
|
+
"args": ["best-mcp"],
|
|
172
|
+
"env": {
|
|
173
|
+
"BEST_TRADING_BASE_URL": "https://api.example.com/best",
|
|
174
|
+
"BEST_TRADING_API_KEY": "your-api-key",
|
|
175
|
+
"BEST_TRADING_TENANT_ID": "your-tenant-id",
|
|
176
|
+
"BEST_TRADING_AUTH_TYPE": "apikey"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
### Mode 2 — `BEST_CONNECTIONS` JSON array
|
|
186
|
+
|
|
187
|
+
For advanced scenarios where per-app vars are not flexible enough. Set `BEST_CONNECTIONS` to a JSON array of connection objects — each connection is fully explicit with no auto-generation.
|
|
188
|
+
|
|
189
|
+
Each object:
|
|
190
|
+
|
|
191
|
+
| Field | Required | Default | Description |
|
|
192
|
+
|---|---|---|---|
|
|
193
|
+
| `name` | yes | — | Connection identifier used in the `connection` tool parameter |
|
|
194
|
+
| `endpoint` | yes | — | Fully-resolved base URL (no `{tenantId}` placeholder) |
|
|
195
|
+
| `apiKey` | yes* | — | Credential (*not required when `authType` is `none`) |
|
|
196
|
+
| `authType` | no | `bearer` | `bearer` · `apikey` · `none` |
|
|
197
|
+
| `authHeader` | no | `X-Api-Key` | Header name when `authType=apikey` and `authIn=header` |
|
|
198
|
+
| `authIn` | no | `header` | `header` or `query` |
|
|
199
|
+
| `authParam` | no | `apikey` | Query param name when `authIn=query` |
|
|
200
|
+
| `allowBearerPassthrough` | no | `false` | Allow a per-request `Authorization: Bearer <token>` header to be forwarded to the BEST endpoint — see [Per-request credential overrides](#http--per-request-credential-overrides-multi-user-backends) |
|
|
201
|
+
| `description` | no | — | Human-readable description surfaced to the LLM for connection selection |
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
### Mode 3 — Legacy single connection
|
|
206
|
+
|
|
207
|
+
For simple single-endpoint setups. Use the flat `BEST_*` variables:
|
|
208
|
+
|
|
209
|
+
| Variable | Required | Default | Description |
|
|
210
|
+
|---|---|---|---|
|
|
211
|
+
| `BEST_ENDPOINT` | yes | — | Base URL of the BEST HTTP surface |
|
|
212
|
+
| `BEST_API_KEY` | yes* | — | Credential (*not required when `BEST_AUTH_TYPE=none`) |
|
|
213
|
+
| `BEST_AUTH_TYPE` | no | `bearer` | `bearer` · `apikey` · `none` |
|
|
214
|
+
| `BEST_AUTH_HEADER` | no | `X-Api-Key` | Header name when `AUTH_TYPE=apikey` |
|
|
215
|
+
| `BEST_AUTH_IN` | no | `header` | `header` or `query` |
|
|
216
|
+
| `BEST_AUTH_PARAM` | no | `apikey` | Query param name when `AUTH_IN=query` |
|
|
217
|
+
| `BEST_ALLOW_BEARER_PASSTHROUGH` | no | `false` | Allow a per-request `Authorization: Bearer <token>` header to be forwarded to the BEST endpoint — see [Per-request credential overrides](#http--per-request-credential-overrides-multi-user-backends) |
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Transport options
|
|
222
|
+
|
|
223
|
+
### stdio — VS Code Copilot, Cursor, Claude Desktop
|
|
224
|
+
|
|
225
|
+
`MCP_TRANSPORT` defaults to `stdio`. Add to your client's MCP config (see [Mode 1](#mode-1--per-app-env-vars-recommended) example above).
|
|
226
|
+
|
|
227
|
+
### HTTP — ChatGPT Desktop
|
|
228
|
+
|
|
229
|
+
Start in HTTP mode and expose via a tunnel:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
MCP_TRANSPORT=http MCP_HTTP_PORT=3001 \
|
|
233
|
+
BEST_TRADING_BASE_URL=https://api.example.com/best \
|
|
234
|
+
BEST_TRADING_API_KEY=<key> \
|
|
235
|
+
BEST_TRADING_AUTH_TYPE=apikey \
|
|
236
|
+
node dist/index.js
|
|
237
|
+
|
|
238
|
+
ngrok http 3001
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Then in ChatGPT Desktop: **Settings → Apps & Connectors → Create**, connector URL: `https://<subdomain>.ngrok.app/mcp`
|
|
242
|
+
|
|
243
|
+
### HTTP — per-request credential overrides (multi-user backends)
|
|
244
|
+
|
|
245
|
+
A backend that calls best-mcp on behalf of many different logged-in users (e.g. a chat assistant) can't bake one fixed API key into the server's environment — it needs to supply the *current* caller's credentials on every request. When `MCP_TRANSPORT=http`, three optional request headers override the resolved connection for that single call only:
|
|
246
|
+
|
|
247
|
+
| Header | Effect |
|
|
248
|
+
|---|---|
|
|
249
|
+
| `X-Api-Key` | Replaces the connection's configured `apiKey` for this request. |
|
|
250
|
+
| `X-Tenant-Id` | Replaces the tenant segment of the endpoint for this request. Only applies to a Mode 1 `<app>/tenant` connection (the one generated from `BEST_<APP>_TENANT_ID`) — ignored on connections with no tenant template. Must match `^[A-Za-z0-9_.-]+$`; an invalid value is ignored (and logged) rather than spliced into the URL. |
|
|
251
|
+
| `Authorization: Bearer <token>` | Forwarded verbatim to the BEST endpoint as the caller's own credential (e.g. a session JWT for a BEST surface that accepts JWTs) — **only when the connection is explicitly configured with `allowBearerPassthrough`** (`BEST_<APP>_ALLOW_BEARER_PASSTHROUGH=true` / `allowBearerPassthrough: true` / `BEST_ALLOW_BEARER_PASSTHROUGH=true`). Bearer scheme only. When forwarded, the effective auth for that request becomes `Authorization: Bearer <token>` regardless of the configured `authType`, so the token can never land in a query string or custom header. |
|
|
252
|
+
|
|
253
|
+
No override header is required — omit them all and a request behaves exactly as configured via environment variables. This has no effect on stdio (there's no per-request boundary to attach headers to).
|
|
254
|
+
|
|
255
|
+
**Precedence:** an explicit per-request `X-Api-Key` always wins; the `Authorization` Bearer token is only used when no `X-Api-Key` is present. This mirrors BEST dual-auth gates, where a present API key is authoritative and never falls through to the JWT.
|
|
256
|
+
|
|
257
|
+
**Security — why Bearer passthrough is opt-in (default off):** on the MCP HTTP transport, the `Authorization` header may carry a credential intended for *this server* (e.g. MCP OAuth between the client and best-mcp). Forwarding it upstream by default would leak that credential across a trust boundary. Enable passthrough only when the MCP caller and the BEST endpoint share one trust domain — i.e. the token the caller sends *is* the credential the BEST service expects. The token is only ever sent to the connection's configured endpoint, over the transport that endpoint's URL specifies (use HTTPS), and is never logged. If a request carries a Bearer token while passthrough is disabled, best-mcp falls back to the configured credential and logs a one-time warning per connection (without the token) so the misconfiguration is diagnosable.
|
|
258
|
+
|
|
259
|
+
**Fail-closed tip:** for a multi-user deployment where *every* request must carry per-caller credentials, keep the connection's configured `apiKey` set to a deliberately invalid placeholder (e.g. `invalid-set-x-api-key-per-request`). A request that arrives without credentials then fails authentication at the BEST service instead of silently acting as a shared identity.
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
curl -X POST http://localhost:3001/mcp \
|
|
263
|
+
-H "Content-Type: application/json" \
|
|
264
|
+
-H "Accept: application/json, text/event-stream" \
|
|
265
|
+
-H "X-Api-Key: <the current user's api key>" \
|
|
266
|
+
-H "X-Tenant-Id: <the current user's tenant>" \
|
|
267
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"execute_query","arguments":{"connection":"trading/tenant","schema":"list-brokers","params":{}}}}'
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Or, with `BEST_TRADING_ALLOW_BEARER_PASSTHROUGH=true`, authenticating the caller with their session JWT instead of an API key:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
curl -X POST http://localhost:3001/mcp \
|
|
274
|
+
-H "Content-Type: application/json" \
|
|
275
|
+
-H "Accept: application/json, text/event-stream" \
|
|
276
|
+
-H "Authorization: Bearer <the current user's session JWT>" \
|
|
277
|
+
-H "X-Tenant-Id: <the current user's tenant>" \
|
|
278
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"execute_query","arguments":{"connection":"trading/tenant","schema":"list-brokers","params":{}}}}'
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## CloudEvent `source` field
|
|
284
|
+
|
|
285
|
+
When sending a command, `send_command` requires a `source` value. The required value is documented in the schema `description` returned by `get_command_schema` — always read it from there, never invent it.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Publishing to npm
|
|
290
|
+
|
|
291
|
+
Never run `npm publish` directly — the release is fully automated via CI:
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
git tag -a mcp/v<x.y.z> -m "Release mcp/v<x.y.z>"
|
|
295
|
+
git push origin mcp/v<x.y.z>
|
|
296
|
+
```
|