@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
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createMcpHandler,
|
|
3
|
+
fromJsonSchema,
|
|
4
|
+
McpServer
|
|
5
|
+
} from "@modelcontextprotocol/server";
|
|
6
|
+
import {
|
|
7
|
+
toNodeHandler
|
|
8
|
+
} from "@modelcontextprotocol/node";
|
|
9
|
+
import {
|
|
10
|
+
createInitialRequestContext,
|
|
11
|
+
MemoryCache,
|
|
12
|
+
SessionSchema
|
|
13
|
+
} from "@reactionary/core";
|
|
14
|
+
import * as z from "zod";
|
|
15
|
+
import {
|
|
16
|
+
discoverReactionaryMCPTools
|
|
17
|
+
} from "./tool-discovery.js";
|
|
18
|
+
import {
|
|
19
|
+
REACTIONARY_SHOPPING_AGENT_GUIDE,
|
|
20
|
+
REACTIONARY_SHOPPING_AGENT_GUIDE_PROMPT,
|
|
21
|
+
REACTIONARY_SHOPPING_AGENT_GUIDE_URI
|
|
22
|
+
} from "./shopping-agent-guide.js";
|
|
23
|
+
class ReactionaryMCPServer {
|
|
24
|
+
constructor(clientFactory, options = {}) {
|
|
25
|
+
this.clientFactory = clientFactory;
|
|
26
|
+
this.options = options;
|
|
27
|
+
this.sessionStore = new ReactionaryMCPSessionStore(
|
|
28
|
+
this.options.sessionCache ?? new MemoryCache(),
|
|
29
|
+
this.options.sessionTtlSeconds ?? 60 * 60 * 24
|
|
30
|
+
);
|
|
31
|
+
this.handler = createMcpHandler(
|
|
32
|
+
(ctx) => this.createServer(getMcpSessionId(ctx.requestInfo)),
|
|
33
|
+
this.options.handler
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
clientFactory;
|
|
37
|
+
options;
|
|
38
|
+
handler;
|
|
39
|
+
sessionStore;
|
|
40
|
+
fetch(request, options) {
|
|
41
|
+
const sessionId = getOrCreateMcpSessionId(request);
|
|
42
|
+
const sessionRequest = withMcpSessionId(request, sessionId);
|
|
43
|
+
return this.handler.fetch(sessionRequest, options).then((response) => {
|
|
44
|
+
response.headers.set(MCP_SESSION_ID_HEADER, sessionId);
|
|
45
|
+
return response;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
getHandler() {
|
|
49
|
+
return {
|
|
50
|
+
...this.handler,
|
|
51
|
+
fetch: (request, options) => this.fetch(request, options),
|
|
52
|
+
close: () => this.close()
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
toNodeHandler() {
|
|
56
|
+
return toNodeHandler(this.getHandler());
|
|
57
|
+
}
|
|
58
|
+
close() {
|
|
59
|
+
return this.handler.close();
|
|
60
|
+
}
|
|
61
|
+
discoverTools() {
|
|
62
|
+
return this.discoverToolsForClient(
|
|
63
|
+
this.clientFactory(createInitialRequestContext())
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
async createServer(sessionId) {
|
|
67
|
+
const restoredSession = sessionId ? await this.sessionStore.get(sessionId) : void 0;
|
|
68
|
+
const requestContext = createInitialRequestContext();
|
|
69
|
+
if (restoredSession) {
|
|
70
|
+
requestContext.session = restoredSession;
|
|
71
|
+
}
|
|
72
|
+
const client = this.clientFactory(requestContext);
|
|
73
|
+
const server = new McpServer({
|
|
74
|
+
name: this.options.name ?? "@reactionary/mcp",
|
|
75
|
+
version: this.options.version ?? "0.0.1"
|
|
76
|
+
});
|
|
77
|
+
this.registerShoppingAgentGuide(server);
|
|
78
|
+
for (const tool of this.discoverToolsForClient(client)) {
|
|
79
|
+
this.registerTool(server, tool, sessionId, requestContext);
|
|
80
|
+
}
|
|
81
|
+
return server;
|
|
82
|
+
}
|
|
83
|
+
discoverToolsForClient(client) {
|
|
84
|
+
return discoverReactionaryMCPTools(client, this.options);
|
|
85
|
+
}
|
|
86
|
+
registerShoppingAgentGuide(server) {
|
|
87
|
+
server.registerResource(
|
|
88
|
+
"reactionary-shopping-agent-guide",
|
|
89
|
+
REACTIONARY_SHOPPING_AGENT_GUIDE_URI,
|
|
90
|
+
{
|
|
91
|
+
title: "Reactionary Shopping Agent Guide",
|
|
92
|
+
description: "Operational guide for combining Reactionary MCP shopping tools safely.",
|
|
93
|
+
mimeType: "text/markdown"
|
|
94
|
+
},
|
|
95
|
+
(uri) => ({
|
|
96
|
+
contents: [
|
|
97
|
+
{
|
|
98
|
+
uri: uri.href,
|
|
99
|
+
mimeType: "text/markdown",
|
|
100
|
+
text: REACTIONARY_SHOPPING_AGENT_GUIDE
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
server.registerPrompt(
|
|
106
|
+
REACTIONARY_SHOPPING_AGENT_GUIDE_PROMPT,
|
|
107
|
+
{
|
|
108
|
+
title: "Reactionary Shopping Agent Guide",
|
|
109
|
+
description: "Use this prompt to learn the recommended search, product, cart, checkout, and guardrail flow for Reactionary MCP tools."
|
|
110
|
+
},
|
|
111
|
+
() => ({
|
|
112
|
+
messages: [
|
|
113
|
+
{
|
|
114
|
+
role: "user",
|
|
115
|
+
content: {
|
|
116
|
+
type: "text",
|
|
117
|
+
text: REACTIONARY_SHOPPING_AGENT_GUIDE
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
registerTool(server, tool, sessionId, requestContext) {
|
|
125
|
+
const inputSchema = acceptsUndefined(tool.entrypoint.inputSchema) ? void 0 : toMcpSchema(tool.entrypoint.inputSchema);
|
|
126
|
+
server.registerTool(
|
|
127
|
+
tool.name,
|
|
128
|
+
{
|
|
129
|
+
title: tool.entrypoint.title,
|
|
130
|
+
description: tool.entrypoint.description,
|
|
131
|
+
inputSchema,
|
|
132
|
+
outputSchema: toMcpOutputSchema(tool.entrypoint.outputSchema)
|
|
133
|
+
},
|
|
134
|
+
async (args) => {
|
|
135
|
+
const result = await callReactionaryTool(
|
|
136
|
+
tool,
|
|
137
|
+
inputSchema ? args : void 0
|
|
138
|
+
);
|
|
139
|
+
if (sessionId) {
|
|
140
|
+
await this.sessionStore.put(sessionId, requestContext.session);
|
|
141
|
+
}
|
|
142
|
+
return resultToCallToolResult(result);
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const MCP_SESSION_ID_HEADER = "mcp-session-id";
|
|
148
|
+
const SESSION_CACHE_KEY_PREFIX = "reactionary:mcp:session";
|
|
149
|
+
class ReactionaryMCPSessionStore {
|
|
150
|
+
constructor(cache, ttlSeconds) {
|
|
151
|
+
this.cache = cache;
|
|
152
|
+
this.ttlSeconds = ttlSeconds;
|
|
153
|
+
}
|
|
154
|
+
cache;
|
|
155
|
+
ttlSeconds;
|
|
156
|
+
async get(sessionId) {
|
|
157
|
+
return await this.cache.get(
|
|
158
|
+
this.getCacheKey(sessionId),
|
|
159
|
+
SessionSchema
|
|
160
|
+
) ?? void 0;
|
|
161
|
+
}
|
|
162
|
+
async put(sessionId, session) {
|
|
163
|
+
await this.cache.invalidate([this.getDependencyId(sessionId)]);
|
|
164
|
+
await this.cache.put(this.getCacheKey(sessionId), session, {
|
|
165
|
+
ttlSeconds: this.ttlSeconds,
|
|
166
|
+
dependencyIds: [this.getDependencyId(sessionId)]
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
getCacheKey(sessionId) {
|
|
170
|
+
return `${SESSION_CACHE_KEY_PREFIX}:${sessionId}`;
|
|
171
|
+
}
|
|
172
|
+
getDependencyId(sessionId) {
|
|
173
|
+
return `${SESSION_CACHE_KEY_PREFIX}:${sessionId}`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function getMcpSessionId(request) {
|
|
177
|
+
return request?.headers.get(MCP_SESSION_ID_HEADER) ?? void 0;
|
|
178
|
+
}
|
|
179
|
+
function getOrCreateMcpSessionId(request) {
|
|
180
|
+
return getMcpSessionId(request) ?? crypto.randomUUID();
|
|
181
|
+
}
|
|
182
|
+
function withMcpSessionId(request, sessionId) {
|
|
183
|
+
if (getMcpSessionId(request) === sessionId) {
|
|
184
|
+
return request;
|
|
185
|
+
}
|
|
186
|
+
const headers = new Headers(request.headers);
|
|
187
|
+
headers.set(MCP_SESSION_ID_HEADER, sessionId);
|
|
188
|
+
return new Request(request, { headers });
|
|
189
|
+
}
|
|
190
|
+
function acceptsUndefined(schema) {
|
|
191
|
+
return schema.safeParse(void 0).success;
|
|
192
|
+
}
|
|
193
|
+
function toMcpSchema(schema) {
|
|
194
|
+
return fromJsonSchema(
|
|
195
|
+
z.toJSONSchema(prepareForJsonSchema(schema), {
|
|
196
|
+
io: "input"
|
|
197
|
+
})
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
function toMcpOutputSchema(schema) {
|
|
201
|
+
return toMcpSchema(z.object({
|
|
202
|
+
value: normalizeOutputValueSchema(schema)
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
function normalizeOutputValueSchema(schema) {
|
|
206
|
+
if (schema instanceof z.ZodVoid || schema instanceof z.ZodUndefined) {
|
|
207
|
+
return z.null();
|
|
208
|
+
}
|
|
209
|
+
return schema.nullable();
|
|
210
|
+
}
|
|
211
|
+
function prepareForJsonSchema(schema) {
|
|
212
|
+
const def = getZodDef(schema);
|
|
213
|
+
switch (def.type) {
|
|
214
|
+
case "default":
|
|
215
|
+
return applySafeDefault(schema, prepareForJsonSchema(def.innerType));
|
|
216
|
+
case "object":
|
|
217
|
+
return copyMetadata(schema, z.looseObject(prepareShapeForJsonSchema(def.shape)));
|
|
218
|
+
case "array":
|
|
219
|
+
return copyMetadata(schema, z.array(prepareForJsonSchema(def.element)));
|
|
220
|
+
case "optional":
|
|
221
|
+
return copyMetadata(schema, prepareForJsonSchema(def.innerType).optional());
|
|
222
|
+
case "nullable":
|
|
223
|
+
return copyMetadata(schema, prepareForJsonSchema(def.innerType).nullable());
|
|
224
|
+
case "union":
|
|
225
|
+
return copyMetadata(schema, prepareUnionForJsonSchema(def.options));
|
|
226
|
+
default:
|
|
227
|
+
return schema;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function applySafeDefault(schema, preparedInnerType) {
|
|
231
|
+
const defaultValue = getSafeDefaultValue(schema);
|
|
232
|
+
if (!defaultValue.success) {
|
|
233
|
+
return copyMetadata(schema, preparedInnerType.optional());
|
|
234
|
+
}
|
|
235
|
+
return copyMetadata(
|
|
236
|
+
schema,
|
|
237
|
+
preparedInnerType.default(defaultValue.value)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
function getSafeDefaultValue(schema) {
|
|
241
|
+
try {
|
|
242
|
+
return {
|
|
243
|
+
success: true,
|
|
244
|
+
value: getZodDef(schema).defaultValue
|
|
245
|
+
};
|
|
246
|
+
} catch {
|
|
247
|
+
return { success: false };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function copyMetadata(source, target) {
|
|
251
|
+
const metadata = source.meta();
|
|
252
|
+
return metadata ? target.meta(metadata) : target;
|
|
253
|
+
}
|
|
254
|
+
function getZodDef(schema) {
|
|
255
|
+
return schema._zod.def;
|
|
256
|
+
}
|
|
257
|
+
function prepareShapeForJsonSchema(shape) {
|
|
258
|
+
return Object.fromEntries(
|
|
259
|
+
Object.entries(shape).map(([key, value]) => [
|
|
260
|
+
key,
|
|
261
|
+
prepareForJsonSchema(value)
|
|
262
|
+
])
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
function prepareUnionForJsonSchema(options) {
|
|
266
|
+
const preparedOptions = options.map(prepareForJsonSchema);
|
|
267
|
+
if (preparedOptions.length < 2) {
|
|
268
|
+
return preparedOptions[0] ?? z.unknown();
|
|
269
|
+
}
|
|
270
|
+
return z.union(
|
|
271
|
+
preparedOptions
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
async function callReactionaryTool(tool, input) {
|
|
275
|
+
const method = Reflect.get(
|
|
276
|
+
tool.capability,
|
|
277
|
+
tool.entrypoint.methodName
|
|
278
|
+
);
|
|
279
|
+
if (typeof method !== "function") {
|
|
280
|
+
throw new Error(`Reactionary MCP tool method not found: ${tool.name}`);
|
|
281
|
+
}
|
|
282
|
+
const result = await Reflect.apply(method, tool.capability, [input]);
|
|
283
|
+
if (!isReactionaryResult(result)) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`Reactionary MCP tool method did not return a Result: ${tool.name}`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
function isReactionaryResult(value) {
|
|
291
|
+
return typeof value === "object" && value !== null && "success" in value && typeof value.success === "boolean";
|
|
292
|
+
}
|
|
293
|
+
function resultToCallToolResult(result) {
|
|
294
|
+
if (!result.success) {
|
|
295
|
+
return {
|
|
296
|
+
isError: true,
|
|
297
|
+
content: [
|
|
298
|
+
{
|
|
299
|
+
type: "text",
|
|
300
|
+
text: JSON.stringify(result.error)
|
|
301
|
+
}
|
|
302
|
+
]
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
structuredContent: toMcpStructuredContent(result.value),
|
|
307
|
+
content: [
|
|
308
|
+
{
|
|
309
|
+
type: "text",
|
|
310
|
+
text: JSON.stringify(toMcpStructuredContent(result.value))
|
|
311
|
+
}
|
|
312
|
+
]
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function toMcpStructuredContent(value) {
|
|
316
|
+
return {
|
|
317
|
+
value: value ?? null
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
export {
|
|
321
|
+
ReactionaryMCPServer
|
|
322
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const REACTIONARY_SHOPPING_AGENT_GUIDE_URI = "reactionary://mcp/guide/shopping-agent";
|
|
2
|
+
const REACTIONARY_SHOPPING_AGENT_GUIDE_PROMPT = "reactionary-shopping-agent-guide";
|
|
3
|
+
const REACTIONARY_SHOPPING_AGENT_GUIDE = `# Reactionary MCP Shopping Agent Guide
|
|
4
|
+
|
|
5
|
+
This guide explains how an agent should combine Reactionary MCP tools when helping a shopper. It is based on the Reactionary documentation under documentation/docs, especially product data, product search, cart, checkout, and query/mutation conventions.
|
|
6
|
+
|
|
7
|
+
## Core mental model
|
|
8
|
+
|
|
9
|
+
Reactionary is a stateless commerce abstraction layer. It exposes vendor-independent capabilities such as product data, product search, price, inventory, cart, checkout, identity, orders, and personalization. The host application owns session state, routing, user confirmation, and business policy.
|
|
10
|
+
|
|
11
|
+
Each MCP tool maps to a decorated Reactionary capability method. Tool inputs are object payloads:
|
|
12
|
+
|
|
13
|
+
- Queries read data.
|
|
14
|
+
- Mutations change state.
|
|
15
|
+
- List queries should use pagination options. Reactionary avoids unbounded list requests.
|
|
16
|
+
|
|
17
|
+
Do not invent identifiers. Product, SKU, variant, cart, checkout, customer, company, and order identifiers must come from prior tool results or explicit user-provided data.
|
|
18
|
+
|
|
19
|
+
## General safety rules
|
|
20
|
+
|
|
21
|
+
1. Prefer read-only tools before mutation tools.
|
|
22
|
+
2. Treat cart, checkout, identity, profile, company, employee, and order-changing tools as state-changing.
|
|
23
|
+
3. Ask the user before making a state-changing cart or checkout operation unless the user clearly requested that exact change.
|
|
24
|
+
4. Verify the selected product variant/SKU before adding it to a cart.
|
|
25
|
+
5. Verify price and inventory when those tools are available, especially before cart mutation or checkout handoff.
|
|
26
|
+
6. After a mutation, read back the resulting object or use the returned object as the new source of truth.
|
|
27
|
+
7. If a tool returns an error, do not guess the missing data. Explain what failed and ask for clarification or retry with corrected inputs.
|
|
28
|
+
8. If search results are ambiguous, ask the user to choose before adding to cart.
|
|
29
|
+
|
|
30
|
+
## Product discovery flow
|
|
31
|
+
|
|
32
|
+
Use this flow for open-ended shopper requests such as "find coffee mugs" or "show me running shoes":
|
|
33
|
+
|
|
34
|
+
1. Call productSearch.queryByTerm with the user's search term and pagination options.
|
|
35
|
+
2. Present a small set of relevant products. Include names, visible variant/SKU information, and facets if useful.
|
|
36
|
+
3. If the user selects a product, use product detail tools such as product.getBySlug, product.getBySKU, or product.getById to obtain the exact product and buyable variant.
|
|
37
|
+
4. Remember that Reactionary uses a Product/Variant model. A variant is the buyable SKU. Search results are optimized for product listing pages and may contain only enough variant data for display and selection.
|
|
38
|
+
5. Do not add a product to cart from a search result unless the result clearly identifies the intended buyable variant.
|
|
39
|
+
|
|
40
|
+
## Product detail flow
|
|
41
|
+
|
|
42
|
+
Use product detail tools when the user names a specific product, slug, SKU, or when a prior search result needs exact data:
|
|
43
|
+
|
|
44
|
+
1. Use product.getBySlug for PDP-style links or SEO slugs.
|
|
45
|
+
2. Use product.getBySKU when the cart or user gives a SKU/variant.
|
|
46
|
+
3. Use product.getById only when the identifier came from Reactionary or the user explicitly provides it.
|
|
47
|
+
4. When a product has multiple variants, ask the user to choose the intended variant unless one is already selected.
|
|
48
|
+
5. For cart and checkout items, prefer SKU/variant identifiers from the item itself and hydrate display data with product.getBySKU if needed.
|
|
49
|
+
|
|
50
|
+
## Price and inventory flow
|
|
51
|
+
|
|
52
|
+
Product search results do not necessarily include authoritative price and inventory. When price or inventory tools are available:
|
|
53
|
+
|
|
54
|
+
1. Use price.getCustomerPrice when the current shopper context may affect pricing.
|
|
55
|
+
2. Use price.getListPrice when a public/list price is enough.
|
|
56
|
+
3. Use inventory tools before promising availability.
|
|
57
|
+
4. If price or stock cannot be verified, state that uncertainty clearly before proceeding.
|
|
58
|
+
5. Re-check before checkout or payment handoff if the cart may be stale.
|
|
59
|
+
|
|
60
|
+
## Cart flow
|
|
61
|
+
|
|
62
|
+
Reactionary supports multiple concurrent carts. There is no universal "active cart"; the host application or shopper chooses which cart to use.
|
|
63
|
+
|
|
64
|
+
Recommended cart flow:
|
|
65
|
+
|
|
66
|
+
1. If a cart identifier is already known, use it.
|
|
67
|
+
2. If not, list carts if the relevant cart capability is available and the user's context supports it.
|
|
68
|
+
3. Do not create an empty placeholder cart just to browse. Create a cart when the shopper actually wants to add something.
|
|
69
|
+
4. Before adding, resolve the product to an exact variant/SKU and quantity.
|
|
70
|
+
5. Optionally verify price and inventory.
|
|
71
|
+
6. Call the cart add/update mutation.
|
|
72
|
+
7. Use the returned cart or read the cart again.
|
|
73
|
+
8. Summarize the resulting cart: item, quantity, estimated price/total when available, and next actions.
|
|
74
|
+
|
|
75
|
+
For add-to-cart, the expected pattern is:
|
|
76
|
+
|
|
77
|
+
1. Product search or detail lookup.
|
|
78
|
+
2. Variant/SKU selection.
|
|
79
|
+
3. Cart create or cart selection.
|
|
80
|
+
4. Cart add.
|
|
81
|
+
5. Cart read/summarize.
|
|
82
|
+
|
|
83
|
+
## Checkout flow
|
|
84
|
+
|
|
85
|
+
In Reactionary, the cart records product selections and calculates price. Checkout is a separate session created from a cart. Once checkout starts, consider the cart read-only unless project-specific rules say otherwise.
|
|
86
|
+
|
|
87
|
+
Recommended checkout flow:
|
|
88
|
+
|
|
89
|
+
1. Ensure the user explicitly wants to start checkout.
|
|
90
|
+
2. Read or use the latest cart.
|
|
91
|
+
3. Verify price and inventory where tools exist.
|
|
92
|
+
4. Initiate checkout from the cart.
|
|
93
|
+
5. Use checkout tools to set shipping address, shipping instruction, payment instruction, and finalize according to available capabilities.
|
|
94
|
+
6. Never claim payment has succeeded unless the relevant checkout/payment tool confirms it.
|
|
95
|
+
|
|
96
|
+
For the current minimal demo scope, checkout/payment handoff may be intentionally disabled. In that case, stop after cart creation/addition and explain that checkout is not available in this MCP surface.
|
|
97
|
+
|
|
98
|
+
## Pagination and list handling
|
|
99
|
+
|
|
100
|
+
Reactionary list queries use paginationOptions with 1-based pages. Prefer small page sizes for agent interactions, for example 5 to 12 items. Do not request unbounded lists.
|
|
101
|
+
|
|
102
|
+
When refining product search:
|
|
103
|
+
|
|
104
|
+
1. Keep the prior search state where possible.
|
|
105
|
+
2. Add selected facets/filters.
|
|
106
|
+
3. Reset pageNumber to 1 after changing filters.
|
|
107
|
+
4. Use additional pages only when the user asks for more.
|
|
108
|
+
|
|
109
|
+
## B2B considerations
|
|
110
|
+
|
|
111
|
+
Some carts, prices, catalogs, lists, and checkout permissions may depend on company context. Do not move a cart from one company to another. If the user asks to buy for a company, make sure company context and permissions are clear before mutating carts or checkout.
|
|
112
|
+
|
|
113
|
+
## Error handling
|
|
114
|
+
|
|
115
|
+
Reactionary tools return structured success/failure results through MCP. If a tool fails:
|
|
116
|
+
|
|
117
|
+
- Do not silently continue as if the operation succeeded.
|
|
118
|
+
- Do not fabricate missing identifiers or prices.
|
|
119
|
+
- Explain the failing step in shopper-friendly language.
|
|
120
|
+
- Ask for a correction when the problem is ambiguous.
|
|
121
|
+
- Retry only with changed inputs or when the failure is clearly transient.
|
|
122
|
+
|
|
123
|
+
## Suggested agent behavior
|
|
124
|
+
|
|
125
|
+
Be concise and transactional. For shopping flows, users usually need:
|
|
126
|
+
|
|
127
|
+
- A short result list.
|
|
128
|
+
- A clear recommendation or disambiguation question.
|
|
129
|
+
- Exact confirmation before mutation.
|
|
130
|
+
- A post-mutation summary.
|
|
131
|
+
|
|
132
|
+
Avoid exposing implementation details unless the user asks.
|
|
133
|
+
`;
|
|
134
|
+
export {
|
|
135
|
+
REACTIONARY_SHOPPING_AGENT_GUIDE,
|
|
136
|
+
REACTIONARY_SHOPPING_AGENT_GUIDE_PROMPT,
|
|
137
|
+
REACTIONARY_SHOPPING_AGENT_GUIDE_URI
|
|
138
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseCapability,
|
|
3
|
+
getReactionaryEntrypoints
|
|
4
|
+
} from "@reactionary/core";
|
|
5
|
+
const defaultToolName = (capabilityName, methodName) => `${capabilityName}.${methodName}`;
|
|
6
|
+
function discoverReactionaryMCPTools(client, options = {}) {
|
|
7
|
+
const toolName = options.toolName ?? defaultToolName;
|
|
8
|
+
return Object.values(client).flatMap((value) => {
|
|
9
|
+
if (!(value instanceof BaseCapability)) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
return getReactionaryEntrypoints(value).map((entrypoint) => ({
|
|
13
|
+
name: toolName(entrypoint.capabilityName, entrypoint.methodName),
|
|
14
|
+
capability: value,
|
|
15
|
+
entrypoint
|
|
16
|
+
}));
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
discoverReactionaryMCPTools
|
|
21
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reactionary/mcp",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./index.js",
|
|
6
|
+
"types": "./src/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"reactionary-mcp": "./cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node --loader @swc-node/register/esm src/cli.ts",
|
|
12
|
+
"inspect": "node --loader @swc-node/register/esm src/inspect.ts"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/Solteq/reactionary.git"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@modelcontextprotocol/node": "2.1.0",
|
|
20
|
+
"@modelcontextprotocol/server": "2.2.0",
|
|
21
|
+
"@reactionary/algolia": "0.0.1",
|
|
22
|
+
"@reactionary/commercetools": "0.0.1",
|
|
23
|
+
"@reactionary/core": "0.0.1",
|
|
24
|
+
"@reactionary/fake": "0.0.1",
|
|
25
|
+
"@reactionary/magento": "0.0.1",
|
|
26
|
+
"@reactionary/medusa": "0.0.1",
|
|
27
|
+
"@reactionary/meilisearch": "0.0.1",
|
|
28
|
+
"@reactionary/unomi": "0.0.1",
|
|
29
|
+
"dotenv": "^17.2.2",
|
|
30
|
+
"zod": "4.1.9"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@swc-node/register": "1.11.1",
|
|
34
|
+
"vitest": "^4.0.9"
|
|
35
|
+
},
|
|
36
|
+
"sideEffects": false
|
|
37
|
+
}
|
package/src/cli.d.ts
ADDED
package/src/index.d.ts
ADDED
package/src/inspect.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Cache, type RequestContext } from '@reactionary/core';
|
|
2
|
+
import type { ReactionaryMCPClient } from './tool-discovery.js';
|
|
3
|
+
export type ReactionaryMCPProviderSystem = 'ALGOLIA' | 'COMMERCETOOLS' | 'FAKE' | 'MAGENTO' | 'MEDUSA' | 'MEILISEARCH' | 'UNOMI';
|
|
4
|
+
export interface CreateReactionaryClientFromEnvOptions {
|
|
5
|
+
env?: NodeJS.ProcessEnv;
|
|
6
|
+
contextOverrides?: Partial<RequestContext>;
|
|
7
|
+
cache?: Cache;
|
|
8
|
+
}
|
|
9
|
+
export interface ReactionaryMCPClientFromEnv {
|
|
10
|
+
client: ReactionaryMCPClient;
|
|
11
|
+
enabledSystems: ReactionaryMCPProviderSystem[];
|
|
12
|
+
}
|
|
13
|
+
export declare function createReactionaryClientFromEnv(options?: CreateReactionaryClientFromEnvOptions): ReactionaryMCPClientFromEnv;
|
|
14
|
+
export declare function getEnabledReactionaryMCPProviderSystems(env?: NodeJS.ProcessEnv): ReactionaryMCPProviderSystem[];
|
|
15
|
+
export declare function getNoEnabledProviderSystemsMessage(): string;
|
|
16
|
+
export declare function parseCommaSeparatedEnvList(value: string | undefined): string[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type CreateMcpHandlerOptions, type McpHttpHandler } from '@modelcontextprotocol/server';
|
|
2
|
+
import { type NodeMcpRequestHandler } from '@modelcontextprotocol/node';
|
|
3
|
+
import { type Cache, type RequestContext } from '@reactionary/core';
|
|
4
|
+
import { type DiscoverReactionaryMCPToolsOptions, type ReactionaryMCPClient, type ReactionaryMCPTool } from './tool-discovery.js';
|
|
5
|
+
export type ReactionaryMCPClientFactory = (requestContext: RequestContext) => ReactionaryMCPClient;
|
|
6
|
+
export interface ReactionaryMCPServerOptions extends DiscoverReactionaryMCPToolsOptions {
|
|
7
|
+
name?: string;
|
|
8
|
+
version?: string;
|
|
9
|
+
sessionCache?: Cache;
|
|
10
|
+
sessionTtlSeconds?: number;
|
|
11
|
+
handler?: Pick<CreateMcpHandlerOptions, 'bus' | 'keepAliveMs' | 'legacy' | 'maxRequestBodySize' | 'maxSubscriptions' | 'onerror' | 'responseMode'>;
|
|
12
|
+
}
|
|
13
|
+
export declare class ReactionaryMCPServer {
|
|
14
|
+
private readonly clientFactory;
|
|
15
|
+
private readonly options;
|
|
16
|
+
private readonly handler;
|
|
17
|
+
private readonly sessionStore;
|
|
18
|
+
constructor(clientFactory: ReactionaryMCPClientFactory, options?: ReactionaryMCPServerOptions);
|
|
19
|
+
fetch(request: Request, options?: Parameters<McpHttpHandler['fetch']>[1]): Promise<Response>;
|
|
20
|
+
getHandler(): McpHttpHandler;
|
|
21
|
+
toNodeHandler(): NodeMcpRequestHandler;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
discoverTools(): ReactionaryMCPTool[];
|
|
24
|
+
private createServer;
|
|
25
|
+
private discoverToolsForClient;
|
|
26
|
+
private registerShoppingAgentGuide;
|
|
27
|
+
private registerTool;
|
|
28
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const REACTIONARY_SHOPPING_AGENT_GUIDE_URI = "reactionary://mcp/guide/shopping-agent";
|
|
2
|
+
export declare const REACTIONARY_SHOPPING_AGENT_GUIDE_PROMPT = "reactionary-shopping-agent-guide";
|
|
3
|
+
export declare const REACTIONARY_SHOPPING_AGENT_GUIDE = "# Reactionary MCP Shopping Agent Guide\n\nThis guide explains how an agent should combine Reactionary MCP tools when helping a shopper. It is based on the Reactionary documentation under documentation/docs, especially product data, product search, cart, checkout, and query/mutation conventions.\n\n## Core mental model\n\nReactionary is a stateless commerce abstraction layer. It exposes vendor-independent capabilities such as product data, product search, price, inventory, cart, checkout, identity, orders, and personalization. The host application owns session state, routing, user confirmation, and business policy.\n\nEach MCP tool maps to a decorated Reactionary capability method. Tool inputs are object payloads:\n\n- Queries read data.\n- Mutations change state.\n- List queries should use pagination options. Reactionary avoids unbounded list requests.\n\nDo not invent identifiers. Product, SKU, variant, cart, checkout, customer, company, and order identifiers must come from prior tool results or explicit user-provided data.\n\n## General safety rules\n\n1. Prefer read-only tools before mutation tools.\n2. Treat cart, checkout, identity, profile, company, employee, and order-changing tools as state-changing.\n3. Ask the user before making a state-changing cart or checkout operation unless the user clearly requested that exact change.\n4. Verify the selected product variant/SKU before adding it to a cart.\n5. Verify price and inventory when those tools are available, especially before cart mutation or checkout handoff.\n6. After a mutation, read back the resulting object or use the returned object as the new source of truth.\n7. If a tool returns an error, do not guess the missing data. Explain what failed and ask for clarification or retry with corrected inputs.\n8. If search results are ambiguous, ask the user to choose before adding to cart.\n\n## Product discovery flow\n\nUse this flow for open-ended shopper requests such as \"find coffee mugs\" or \"show me running shoes\":\n\n1. Call productSearch.queryByTerm with the user's search term and pagination options.\n2. Present a small set of relevant products. Include names, visible variant/SKU information, and facets if useful.\n3. If the user selects a product, use product detail tools such as product.getBySlug, product.getBySKU, or product.getById to obtain the exact product and buyable variant.\n4. Remember that Reactionary uses a Product/Variant model. A variant is the buyable SKU. Search results are optimized for product listing pages and may contain only enough variant data for display and selection.\n5. Do not add a product to cart from a search result unless the result clearly identifies the intended buyable variant.\n\n## Product detail flow\n\nUse product detail tools when the user names a specific product, slug, SKU, or when a prior search result needs exact data:\n\n1. Use product.getBySlug for PDP-style links or SEO slugs.\n2. Use product.getBySKU when the cart or user gives a SKU/variant.\n3. Use product.getById only when the identifier came from Reactionary or the user explicitly provides it.\n4. When a product has multiple variants, ask the user to choose the intended variant unless one is already selected.\n5. For cart and checkout items, prefer SKU/variant identifiers from the item itself and hydrate display data with product.getBySKU if needed.\n\n## Price and inventory flow\n\nProduct search results do not necessarily include authoritative price and inventory. When price or inventory tools are available:\n\n1. Use price.getCustomerPrice when the current shopper context may affect pricing.\n2. Use price.getListPrice when a public/list price is enough.\n3. Use inventory tools before promising availability.\n4. If price or stock cannot be verified, state that uncertainty clearly before proceeding.\n5. Re-check before checkout or payment handoff if the cart may be stale.\n\n## Cart flow\n\nReactionary supports multiple concurrent carts. There is no universal \"active cart\"; the host application or shopper chooses which cart to use.\n\nRecommended cart flow:\n\n1. If a cart identifier is already known, use it.\n2. If not, list carts if the relevant cart capability is available and the user's context supports it.\n3. Do not create an empty placeholder cart just to browse. Create a cart when the shopper actually wants to add something.\n4. Before adding, resolve the product to an exact variant/SKU and quantity.\n5. Optionally verify price and inventory.\n6. Call the cart add/update mutation.\n7. Use the returned cart or read the cart again.\n8. Summarize the resulting cart: item, quantity, estimated price/total when available, and next actions.\n\nFor add-to-cart, the expected pattern is:\n\n1. Product search or detail lookup.\n2. Variant/SKU selection.\n3. Cart create or cart selection.\n4. Cart add.\n5. Cart read/summarize.\n\n## Checkout flow\n\nIn Reactionary, the cart records product selections and calculates price. Checkout is a separate session created from a cart. Once checkout starts, consider the cart read-only unless project-specific rules say otherwise.\n\nRecommended checkout flow:\n\n1. Ensure the user explicitly wants to start checkout.\n2. Read or use the latest cart.\n3. Verify price and inventory where tools exist.\n4. Initiate checkout from the cart.\n5. Use checkout tools to set shipping address, shipping instruction, payment instruction, and finalize according to available capabilities.\n6. Never claim payment has succeeded unless the relevant checkout/payment tool confirms it.\n\nFor the current minimal demo scope, checkout/payment handoff may be intentionally disabled. In that case, stop after cart creation/addition and explain that checkout is not available in this MCP surface.\n\n## Pagination and list handling\n\nReactionary list queries use paginationOptions with 1-based pages. Prefer small page sizes for agent interactions, for example 5 to 12 items. Do not request unbounded lists.\n\nWhen refining product search:\n\n1. Keep the prior search state where possible.\n2. Add selected facets/filters.\n3. Reset pageNumber to 1 after changing filters.\n4. Use additional pages only when the user asks for more.\n\n## B2B considerations\n\nSome carts, prices, catalogs, lists, and checkout permissions may depend on company context. Do not move a cart from one company to another. If the user asks to buy for a company, make sure company context and permissions are clear before mutating carts or checkout.\n\n## Error handling\n\nReactionary tools return structured success/failure results through MCP. If a tool fails:\n\n- Do not silently continue as if the operation succeeded.\n- Do not fabricate missing identifiers or prices.\n- Explain the failing step in shopper-friendly language.\n- Ask for a correction when the problem is ambiguous.\n- Retry only with changed inputs or when the failure is clearly transient.\n\n## Suggested agent behavior\n\nBe concise and transactional. For shopping flows, users usually need:\n\n- A short result list.\n- A clear recommendation or disambiguation question.\n- Exact confirmation before mutation.\n- A post-mutation summary.\n\nAvoid exposing implementation details unless the user asks.\n";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BaseCapability, type ReactionaryEntrypointMetadata } from '@reactionary/core';
|
|
2
|
+
export interface ReactionaryMCPTool {
|
|
3
|
+
name: string;
|
|
4
|
+
capability: BaseCapability;
|
|
5
|
+
entrypoint: ReactionaryEntrypointMetadata;
|
|
6
|
+
}
|
|
7
|
+
export type ReactionaryMCPClient = object;
|
|
8
|
+
export interface DiscoverReactionaryMCPToolsOptions {
|
|
9
|
+
toolName?: (capabilityName: string, methodName: string) => string;
|
|
10
|
+
}
|
|
11
|
+
export declare function discoverReactionaryMCPTools(client: ReactionaryMCPClient, options?: DiscoverReactionaryMCPToolsOptions): ReactionaryMCPTool[];
|