@reactionary/mcp 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +461 -2
- package/cli.js +125 -0
- package/index.js +4 -0
- package/inspect.js +158 -0
- package/lib/env-client-builder.js +283 -0
- package/lib/load-project-root-env.js +40 -0
- package/lib/reactionary-mcp-server.js +322 -0
- package/lib/shopping-agent-guide.js +138 -0
- package/lib/tool-discovery.js +21 -0
- package/package.json +35 -4
- package/src/cli.d.ts +8 -0
- package/src/index.d.ts +4 -0
- package/src/inspect.d.ts +2 -0
- package/src/lib/env-client-builder.d.ts +16 -0
- package/src/lib/load-project-root-env.d.ts +2 -0
- package/src/lib/reactionary-mcp-server.d.ts +28 -0
- package/src/lib/shopping-agent-guide.d.ts +3 -0
- package/src/lib/tool-discovery.d.ts +11 -0
package/README.md
CHANGED
|
@@ -1,3 +1,462 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @reactionary/mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@reactionary/mcp` exposes an instantiated Reactionary client surface as a Model Context Protocol (MCP) HTTP service.
|
|
4
|
+
|
|
5
|
+
The package is intentionally thin:
|
|
6
|
+
|
|
7
|
+
- Reactionary remains the commerce abstraction layer.
|
|
8
|
+
- Host applications own routing, deployment, authentication, policy, and persistence choices.
|
|
9
|
+
- MCP tools are generated from existing Reactionary capability metadata instead of being hand-written one by one.
|
|
10
|
+
- The same runtime can be mounted from frameworks such as Next.js, Express, or plain Node HTTP.
|
|
11
|
+
|
|
12
|
+
## Design goals
|
|
13
|
+
|
|
14
|
+
The adapter exists to make Reactionary usable by agents without turning Reactionary into a hosted platform or router.
|
|
15
|
+
|
|
16
|
+
The important boundary is:
|
|
17
|
+
|
|
18
|
+
- **Reactionary packages** provide typed commerce capabilities.
|
|
19
|
+
- **`@reactionary/mcp`** converts those capabilities into MCP tools, resources, and prompts.
|
|
20
|
+
- **The host app** decides where `/mcp` lives, how users authenticate, how sessions are stored, and which capabilities are enabled.
|
|
21
|
+
|
|
22
|
+
This package therefore exports `ReactionaryMCPServer` instead of adding a built-in `/mcp` route to Reactionary itself.
|
|
23
|
+
|
|
24
|
+
## Core API
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { ReactionaryMCPServer } from '@reactionary/mcp';
|
|
28
|
+
import { createReactionaryClient } from './my-reactionary-client';
|
|
29
|
+
|
|
30
|
+
const mcp = new ReactionaryMCPServer((requestContext) =>
|
|
31
|
+
createReactionaryClient({
|
|
32
|
+
context: requestContext,
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export async function POST(request: Request) {
|
|
37
|
+
return mcp.fetch(request);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`ReactionaryMCPServer` takes a **client factory**, not a singleton client:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
type ReactionaryMCPClientFactory = (
|
|
45
|
+
requestContext: RequestContext,
|
|
46
|
+
) => ReactionaryMCPClient;
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The factory is called for each MCP serving unit with a fresh `RequestContext`. This matters because Reactionary capabilities hold a mutable request context. Identity, cart, checkout, personalization, and provider-specific session data can change during tool calls.
|
|
50
|
+
|
|
51
|
+
Passing a singleton client would let one MCP user/session inherit another user's identity or cart state, so the public runtime is designed around per-request/per-session client construction.
|
|
52
|
+
|
|
53
|
+
## MCP server wrapper
|
|
54
|
+
|
|
55
|
+
Internally, the package uses:
|
|
56
|
+
|
|
57
|
+
- `@modelcontextprotocol/server`
|
|
58
|
+
- `createMcpHandler(...)`
|
|
59
|
+
- `McpServer`
|
|
60
|
+
- `fromJsonSchema(...)`
|
|
61
|
+
- `@modelcontextprotocol/node`
|
|
62
|
+
- `toNodeHandler(...)`
|
|
63
|
+
|
|
64
|
+
`ReactionaryMCPServer` exposes both Web-standard and Node-style integration:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const mcp = new ReactionaryMCPServer(createClient);
|
|
68
|
+
|
|
69
|
+
// Web-standard runtimes: Next.js route handlers, Fetch API servers, Workers-like runtimes
|
|
70
|
+
await mcp.fetch(request);
|
|
71
|
+
|
|
72
|
+
// Node HTTP / Express-style integration
|
|
73
|
+
const nodeHandler = mcp.toNodeHandler();
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The core package does not depend on Express.
|
|
77
|
+
|
|
78
|
+
## Tool discovery
|
|
79
|
+
|
|
80
|
+
MCP tools are discovered from Reactionary capability methods decorated with `@Reactionary(...)`.
|
|
81
|
+
|
|
82
|
+
The metadata comes from core capability runtime discovery:
|
|
83
|
+
|
|
84
|
+
- capability resource name
|
|
85
|
+
- method name
|
|
86
|
+
- input Zod schema
|
|
87
|
+
- output Zod schema
|
|
88
|
+
- title
|
|
89
|
+
- description
|
|
90
|
+
|
|
91
|
+
Default tool names are:
|
|
92
|
+
|
|
93
|
+
```txt
|
|
94
|
+
<capabilityName>.<methodName>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
|
|
99
|
+
```txt
|
|
100
|
+
product.getBySKU
|
|
101
|
+
productSearch.queryByTerm
|
|
102
|
+
price.getListPrice
|
|
103
|
+
inventory.getBySKU
|
|
104
|
+
cart.addItem
|
|
105
|
+
checkout.startCheckout
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Tool names can be customized with the `toolName` option:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const mcp = new ReactionaryMCPServer(createClient, {
|
|
112
|
+
toolName: (capabilityName, methodName) =>
|
|
113
|
+
`reactionary.${capabilityName}.${methodName}`,
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Schema conversion and defaults
|
|
118
|
+
|
|
119
|
+
Reactionary schemas are Zod schemas. MCP expects JSON-Schema-like tool contracts. `@reactionary/mcp` converts schemas with:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
z.toJSONSchema(schema, { io: 'input' })
|
|
123
|
+
fromJsonSchema(...)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
There are two important details:
|
|
127
|
+
|
|
128
|
+
1. **Descriptions are preserved**
|
|
129
|
+
|
|
130
|
+
Zod `.meta({ description })` and compatible descriptions become MCP field descriptions through Zod JSON Schema conversion.
|
|
131
|
+
|
|
132
|
+
2. **Defaults are handled defensively**
|
|
133
|
+
|
|
134
|
+
Some legacy schemas used default factories such as:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
SomeSchema.default(() => SomeSchema.parse({}))
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
That is unsafe when `SomeSchema.parse({})` is invalid. Zod JSON Schema generation can evaluate those default factories, which can crash tool discovery.
|
|
141
|
+
|
|
142
|
+
The MCP adapter now prepares schemas before conversion:
|
|
143
|
+
|
|
144
|
+
- safe defaults are preserved and shown to MCP clients
|
|
145
|
+
- unsafe defaults are omitted from the MCP schema
|
|
146
|
+
- fields with unsafe defaults remain optional to preserve the "can be omitted" intent
|
|
147
|
+
- descriptions/metadata are copied when schemas are rebuilt for JSON Schema generation
|
|
148
|
+
|
|
149
|
+
### Monetary and cost default note
|
|
150
|
+
|
|
151
|
+
Core monetary amounts remain strict: a raw money object must include both `value` and `currency`. Empty provider data such as `{}` should fail output validation rather than silently becoming a real price.
|
|
152
|
+
|
|
153
|
+
Some aggregate cart/checkout cost-breakdown fields still use a temporary placeholder default:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{ "value": 0, "currency": "EUR" }
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
That fallback is intentionally placed on the parent cost-breakdown fields, not on `MonetaryAmountSchema` itself. It should eventually be replaced by configured/request-context currency handling or explicit "unknown monetary amount" modeling.
|
|
160
|
+
|
|
161
|
+
## Tool result envelope
|
|
162
|
+
|
|
163
|
+
Successful MCP tool calls return structured content in a consistent envelope:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"value": {}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Void-returning capabilities use:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"value": null
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
This keeps successful `undefined`, scalar, array, and object results protocol-valid and aligned with the advertised MCP output schema.
|
|
180
|
+
|
|
181
|
+
## MCP session handling
|
|
182
|
+
|
|
183
|
+
Each MCP session must preserve the Reactionary `RequestContext.session` across requests.
|
|
184
|
+
|
|
185
|
+
`ReactionaryMCPServer` handles this by:
|
|
186
|
+
|
|
187
|
+
1. Reading the `mcp-session-id` request header.
|
|
188
|
+
2. Creating a new session id when the request has none.
|
|
189
|
+
3. Creating a fresh `RequestContext`.
|
|
190
|
+
4. Restoring the cached `.session` into that context when a previous session exists.
|
|
191
|
+
5. Building a fresh Reactionary client with that context.
|
|
192
|
+
6. Persisting `requestContext.session` back to cache after each tool call.
|
|
193
|
+
7. Returning `mcp-session-id` in the response headers.
|
|
194
|
+
|
|
195
|
+
Only `.session` is persisted. Other request context fields, such as locale, currency, store, tax jurisdiction, correlation id, IP address, and user agent, are expected to be supplied by the host per request.
|
|
196
|
+
|
|
197
|
+
### Session cache
|
|
198
|
+
|
|
199
|
+
By default, the package uses Reactionary `MemoryCache` for MCP session state:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
const mcp = new ReactionaryMCPServer(createClient);
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
For production or multi-instance deployments, provide any Reactionary `Cache` implementation:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
const mcp = new ReactionaryMCPServer(createClient, {
|
|
209
|
+
sessionCache: redisBackedReactionaryCache,
|
|
210
|
+
sessionTtlSeconds: 60 * 60,
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The cache is used only for the MCP session snapshot. Capability-level caching is still controlled by the Reactionary clients/capabilities created by your factory.
|
|
215
|
+
|
|
216
|
+
### Why not reuse one client?
|
|
217
|
+
|
|
218
|
+
Reactionary capabilities receive a mutable `RequestContext`. Several capabilities read or write session data:
|
|
219
|
+
|
|
220
|
+
- identity login/logout
|
|
221
|
+
- active user identity
|
|
222
|
+
- personalization profile
|
|
223
|
+
- cart-related provider state
|
|
224
|
+
- checkout state
|
|
225
|
+
- provider-specific session keys
|
|
226
|
+
|
|
227
|
+
If the HTTP handler reused a single client instance, all MCP sessions would share that context. That would allow one connected client to inherit another client's identity, token, active cart, or personalization state.
|
|
228
|
+
|
|
229
|
+
The adapter therefore creates a fresh client/context per request and restores only the session snapshot associated with the MCP session id.
|
|
230
|
+
|
|
231
|
+
## Resources and prompts
|
|
232
|
+
|
|
233
|
+
The package exposes a shopping-agent guide as both an MCP resource and an MCP prompt.
|
|
234
|
+
|
|
235
|
+
Resource URI:
|
|
236
|
+
|
|
237
|
+
```txt
|
|
238
|
+
reactionary://mcp/guide/shopping-agent
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Prompt name:
|
|
242
|
+
|
|
243
|
+
```txt
|
|
244
|
+
reactionary-shopping-agent-guide
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The guide explains how agents should combine Reactionary tools for product discovery, product details, price/inventory checks, cart operations, checkout, pagination, B2B context, and error handling.
|
|
248
|
+
|
|
249
|
+
The resource uses a Reactionary-owned URI scheme. Some clients, including VS Code, may show MCP resources internally as `mcp-resource://...`; that is a client-side virtual URI and not the URI the server should advertise.
|
|
250
|
+
|
|
251
|
+
See [`docs/shopping-agent-guide.md`](./docs/shopping-agent-guide.md).
|
|
252
|
+
|
|
253
|
+
## CLI
|
|
254
|
+
|
|
255
|
+
The package includes a small CLI for local demos and MCP Inspector testing.
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
pnpm --dir packages/mcp start
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Defaults:
|
|
262
|
+
|
|
263
|
+
```txt
|
|
264
|
+
MCP_HOST=127.0.0.1
|
|
265
|
+
MCP_PORT=3000
|
|
266
|
+
MCP_PATH=/mcp
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The CLI:
|
|
270
|
+
|
|
271
|
+
1. Loads `.env` from the project root.
|
|
272
|
+
2. Reads enabled provider systems from `ENABLED_<SYSTEM>=true`.
|
|
273
|
+
3. Creates a `ReactionaryMCPServer`.
|
|
274
|
+
4. Serves MCP over streamable HTTP.
|
|
275
|
+
5. Creates a fresh Reactionary client/context per MCP request/session.
|
|
276
|
+
|
|
277
|
+
Supported provider flags:
|
|
278
|
+
|
|
279
|
+
```txt
|
|
280
|
+
ENABLED_FAKE=true
|
|
281
|
+
ENABLED_MEDUSA=true
|
|
282
|
+
ENABLED_MAGENTO=true
|
|
283
|
+
ENABLED_COMMERCETOOLS=true
|
|
284
|
+
ENABLED_ALGOLIA=true
|
|
285
|
+
ENABLED_MEILISEARCH=true
|
|
286
|
+
ENABLED_UNOMI=true
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Example:
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
ENABLED_FAKE=true pnpm --dir packages/mcp start
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Start without Inspector
|
|
296
|
+
|
|
297
|
+
Use this when you want to run the MCP HTTP server directly and connect any MCP-capable client yourself.
|
|
298
|
+
|
|
299
|
+
Fake provider demo:
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
ENABLED_FAKE=true pnpm --dir packages/mcp start
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Custom host, port, and path:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
ENABLED_FAKE=true pnpm --dir packages/mcp start -- --host 127.0.0.1 --port 3333 --path /mcp
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Equivalent environment-variable form:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
ENABLED_FAKE=true MCP_HOST=127.0.0.1 MCP_PORT=3333 MCP_PATH=/mcp pnpm --dir packages/mcp start
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The server will listen at:
|
|
318
|
+
|
|
319
|
+
```txt
|
|
320
|
+
http://127.0.0.1:3333/mcp
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Medusa-backed example:
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
ENABLED_MEDUSA=true \
|
|
327
|
+
MEDUSA_BASE_URL=http://localhost:9000 \
|
|
328
|
+
MEDUSA_PUBLISHABLE_KEY=pk_test_... \
|
|
329
|
+
MEDUSA_ADMIN_KEY=... \
|
|
330
|
+
pnpm --dir packages/mcp start
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
You can also put these variables in the project root `.env`; the CLI loads that file automatically.
|
|
334
|
+
|
|
335
|
+
### Inspector
|
|
336
|
+
|
|
337
|
+
For local development:
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
pnpm --dir packages/mcp inspect
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
This starts the local Reactionary MCP server and opens MCP Inspector against the configured server URL.
|
|
344
|
+
|
|
345
|
+
Fake provider with Inspector:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
ENABLED_FAKE=true pnpm --dir packages/mcp inspect
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Inspector against a custom local endpoint:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
ENABLED_FAKE=true MCP_HOST=127.0.0.1 MCP_PORT=3333 MCP_PATH=/mcp pnpm --dir packages/mcp inspect
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
The inspector launcher starts the Reactionary MCP server first, then launches MCP Inspector using HTTP transport pointed at:
|
|
358
|
+
|
|
359
|
+
```txt
|
|
360
|
+
http://${MCP_HOST}:${MCP_PORT}${MCP_PATH}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
For the defaults, that is:
|
|
364
|
+
|
|
365
|
+
```txt
|
|
366
|
+
http://127.0.0.1:3000/mcp
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Environment-backed client builder
|
|
370
|
+
|
|
371
|
+
`createReactionaryClientFromEnv(...)` is a convenience helper for demos and CLI usage. It mirrors the provider setup logic from the example Node package and enables capabilities based on environment variables.
|
|
372
|
+
|
|
373
|
+
Embedded applications do not have to use this helper. In production, prefer a host-owned client factory:
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
const mcp = new ReactionaryMCPServer((requestContext) => {
|
|
377
|
+
return new ClientBuilder(requestContext)
|
|
378
|
+
.withCache(myCapabilityCache)
|
|
379
|
+
.withCapability(...)
|
|
380
|
+
.build();
|
|
381
|
+
}, {
|
|
382
|
+
sessionCache: mySessionCache,
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Integration examples
|
|
387
|
+
|
|
388
|
+
### Next.js route handler
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
// app/api/mcp/route.ts
|
|
392
|
+
import { ReactionaryMCPServer } from '@reactionary/mcp';
|
|
393
|
+
import { buildReactionaryClient } from '@/lib/reactionary';
|
|
394
|
+
|
|
395
|
+
const mcp = new ReactionaryMCPServer((requestContext) =>
|
|
396
|
+
buildReactionaryClient({ requestContext }),
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
export async function POST(request: Request) {
|
|
400
|
+
return mcp.fetch(request);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export async function GET(request: Request) {
|
|
404
|
+
return mcp.fetch(request);
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Plain Node HTTP
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
import { createServer } from 'node:http';
|
|
412
|
+
import { ReactionaryMCPServer } from '@reactionary/mcp';
|
|
413
|
+
|
|
414
|
+
const mcp = new ReactionaryMCPServer(createClient);
|
|
415
|
+
const handler = mcp.toNodeHandler();
|
|
416
|
+
|
|
417
|
+
createServer(async (request, response) => {
|
|
418
|
+
await handler(request, response);
|
|
419
|
+
}).listen(3000);
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## Guardrails and responsibilities
|
|
423
|
+
|
|
424
|
+
This package provides the MCP transport and generated tool surface. It does not replace application-level commerce policy.
|
|
425
|
+
|
|
426
|
+
Recommended host responsibilities:
|
|
427
|
+
|
|
428
|
+
- authenticate users before exposing sensitive/stateful capabilities
|
|
429
|
+
- authorize tool access by user, role, market, channel, or company
|
|
430
|
+
- rate limit agent traffic
|
|
431
|
+
- verify price and inventory before cart/checkout mutations
|
|
432
|
+
- enforce spend limits
|
|
433
|
+
- audit state-changing tool calls
|
|
434
|
+
- avoid exposing admin-only provider credentials to public agent clients
|
|
435
|
+
|
|
436
|
+
For a first demo, prefer a small surface:
|
|
437
|
+
|
|
438
|
+
1. Product search
|
|
439
|
+
2. Product detail / variant resolution
|
|
440
|
+
3. Price and inventory verification when available
|
|
441
|
+
4. Cart create/add/update
|
|
442
|
+
5. Checkout handoff later
|
|
443
|
+
|
|
444
|
+
## Current limitations and follow-ups
|
|
445
|
+
|
|
446
|
+
- MCP session persistence currently stores only `RequestContext.session`.
|
|
447
|
+
- The default MCP session store is in-memory and not suitable for multi-process production deployments.
|
|
448
|
+
- Some core schemas still contain legacy empty-object default factories for identifiers. The MCP schema converter avoids crashing on those, but core modeling should eventually distinguish required identifiers from optional/unknown values more explicitly.
|
|
449
|
+
- Monetary defaults currently use a temporary `0/EUR` fallback.
|
|
450
|
+
- Checkout/payment/ACP Instant Checkout support is intentionally not implemented in this first MCP package slice.
|
|
451
|
+
|
|
452
|
+
## Validation commands
|
|
453
|
+
|
|
454
|
+
Useful local checks:
|
|
455
|
+
|
|
456
|
+
```bash
|
|
457
|
+
pnpm exec tsc -p packages/mcp/tsconfig.spec.json --noEmit
|
|
458
|
+
pnpm exec vitest run --config packages/mcp/vitest.config.mts
|
|
459
|
+
pnpm exec nx run mcp:lint
|
|
460
|
+
pnpm exec nx run mcp:build
|
|
461
|
+
pnpm exec nx affected -t lint build test:offline
|
|
462
|
+
```
|
package/cli.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { ReactionaryMCPServer } from "./lib/reactionary-mcp-server.js";
|
|
5
|
+
import {
|
|
6
|
+
createReactionaryClientFromEnv,
|
|
7
|
+
getEnabledReactionaryMCPProviderSystems,
|
|
8
|
+
getNoEnabledProviderSystemsMessage
|
|
9
|
+
} from "./lib/env-client-builder.js";
|
|
10
|
+
import { loadProjectRootEnv } from "./lib/load-project-root-env.js";
|
|
11
|
+
async function main() {
|
|
12
|
+
loadProjectRootEnv(import.meta.url);
|
|
13
|
+
const options = parseOptions(process.argv.slice(2), process.env);
|
|
14
|
+
const enabledSystems = getEnabledReactionaryMCPProviderSystems();
|
|
15
|
+
if (enabledSystems.length === 0) {
|
|
16
|
+
throw new Error(getNoEnabledProviderSystemsMessage());
|
|
17
|
+
}
|
|
18
|
+
const mcp = new ReactionaryMCPServer(
|
|
19
|
+
(requestContext) => createReactionaryClientFromEnv({
|
|
20
|
+
contextOverrides: requestContext
|
|
21
|
+
}).client,
|
|
22
|
+
{
|
|
23
|
+
name: process.env["MCP_SERVER_NAME"] || "@reactionary/mcp",
|
|
24
|
+
version: process.env["MCP_SERVER_VERSION"] || "0.0.1",
|
|
25
|
+
handler: {
|
|
26
|
+
onerror: (error) => {
|
|
27
|
+
console.error(error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
const mcpHandler = mcp.toNodeHandler();
|
|
33
|
+
const server = createServer(async (request, response) => {
|
|
34
|
+
const requestUrl = new URL(
|
|
35
|
+
request.url ?? "/",
|
|
36
|
+
`http://${request.headers.host ?? `${options.host}:${options.port}`}`
|
|
37
|
+
);
|
|
38
|
+
if (requestUrl.pathname !== options.path) {
|
|
39
|
+
response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
40
|
+
response.end("Not found");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
await mcpHandler(request, response);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(error);
|
|
47
|
+
if (!response.headersSent) {
|
|
48
|
+
response.writeHead(500, {
|
|
49
|
+
"content-type": "text/plain; charset=utf-8"
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
response.end("Internal server error");
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
server.on("clientError", (error, socket) => {
|
|
56
|
+
console.error(error);
|
|
57
|
+
socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
58
|
+
});
|
|
59
|
+
await new Promise((resolve) => {
|
|
60
|
+
server.listen(options.port, options.host, resolve);
|
|
61
|
+
});
|
|
62
|
+
console.error(
|
|
63
|
+
`Reactionary MCP server listening on http://${options.host}:${options.port}${options.path}`
|
|
64
|
+
);
|
|
65
|
+
console.error(`Enabled systems: ${enabledSystems.join(", ")}`);
|
|
66
|
+
const close = async () => {
|
|
67
|
+
await mcp.close();
|
|
68
|
+
await new Promise((resolve, reject) => {
|
|
69
|
+
server.close((error) => {
|
|
70
|
+
if (error) {
|
|
71
|
+
reject(error);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
resolve();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
process.once("SIGINT", () => {
|
|
79
|
+
void close().finally(() => process.exit(0));
|
|
80
|
+
});
|
|
81
|
+
process.once("SIGTERM", () => {
|
|
82
|
+
void close().finally(() => process.exit(0));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function parseOptions(args, env) {
|
|
86
|
+
const values = /* @__PURE__ */ new Map();
|
|
87
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
88
|
+
const arg = args[index];
|
|
89
|
+
if (!arg.startsWith("--")) {
|
|
90
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
91
|
+
}
|
|
92
|
+
const [rawKey, inlineValue] = arg.slice(2).split("=", 2);
|
|
93
|
+
const value = inlineValue ?? args[index + 1];
|
|
94
|
+
if (!value || value.startsWith("--")) {
|
|
95
|
+
throw new Error(`Missing value for argument: ${arg}`);
|
|
96
|
+
}
|
|
97
|
+
values.set(rawKey, value);
|
|
98
|
+
if (inlineValue === void 0) {
|
|
99
|
+
index += 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const host = values.get("host") ?? env["MCP_HOST"] ?? "127.0.0.1";
|
|
103
|
+
const path = values.get("path") ?? env["MCP_PATH"] ?? "/mcp";
|
|
104
|
+
const rawPort = values.get("port") ?? env["MCP_PORT"] ?? env["PORT"] ?? "3000";
|
|
105
|
+
if (!/^\d+$/.test(rawPort)) {
|
|
106
|
+
throw new Error(`Invalid port: ${rawPort}`);
|
|
107
|
+
}
|
|
108
|
+
const port = Number(rawPort);
|
|
109
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
110
|
+
throw new Error(`Invalid port: ${rawPort}`);
|
|
111
|
+
}
|
|
112
|
+
if (!path.startsWith("/")) {
|
|
113
|
+
throw new Error(`MCP path must start with "/": ${path}`);
|
|
114
|
+
}
|
|
115
|
+
return { host, port, path };
|
|
116
|
+
}
|
|
117
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
118
|
+
main().catch((error) => {
|
|
119
|
+
console.error(error);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
export {
|
|
124
|
+
parseOptions
|
|
125
|
+
};
|
package/index.js
ADDED