@useagents/redop 0.1.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/dist/index.js ADDED
@@ -0,0 +1,925 @@
1
+ // src/adapters/schema.ts
2
+ function hasRecordShape(value) {
3
+ return typeof value === "object" && value !== null;
4
+ }
5
+ function isStandardSchema(schema) {
6
+ if (!hasRecordShape(schema) || !("~standard" in schema))
7
+ return false;
8
+ const standard = schema["~standard"];
9
+ return hasRecordShape(standard) && typeof standard.validate === "function" && standard.version === 1;
10
+ }
11
+ function isJsonSchema(schema) {
12
+ return hasRecordShape(schema) && (("type" in schema) || ("properties" in schema) || ("$schema" in schema));
13
+ }
14
+ function hasJsonSchemaSupport(schema) {
15
+ return typeof schema["~standard"].jsonSchema?.input === "function";
16
+ }
17
+ function createValidationError(issues) {
18
+ const error = new Error("Validation failed");
19
+ error.issues = issues;
20
+ return error;
21
+ }
22
+ function standardSchemaAdapter() {
23
+ return {
24
+ toJsonSchema(schema) {
25
+ if (!hasJsonSchemaSupport(schema)) {
26
+ throw new Error("[redop] Schema provides validation but not JSON Schema generation. Pass an explicit schemaAdapter.");
27
+ }
28
+ return schema["~standard"].jsonSchema.input({
29
+ target: "draft-07"
30
+ });
31
+ },
32
+ async parse(schema, input) {
33
+ const result = await schema["~standard"].validate(input);
34
+ if (result.issues) {
35
+ throw createValidationError(result.issues);
36
+ }
37
+ return result.value;
38
+ }
39
+ };
40
+ }
41
+ function zodAdapter() {
42
+ return standardSchemaAdapter();
43
+ }
44
+ function jsonSchemaAdapter() {
45
+ return {
46
+ toJsonSchema: (schema) => schema,
47
+ parse: (_schema, input) => input
48
+ };
49
+ }
50
+ function detectAdapter(schema) {
51
+ if (isStandardSchema(schema))
52
+ return standardSchemaAdapter();
53
+ if (isJsonSchema(schema))
54
+ return jsonSchemaAdapter();
55
+ throw new Error("[redop] Could not detect schema type. Pass a Standard Schema-compatible instance or a plain JSON Schema object.");
56
+ }
57
+ // src/transports/http.ts
58
+ function createSessionStore(timeoutMs) {
59
+ const sessions = new Map;
60
+ function gc() {
61
+ const now = Date.now();
62
+ for (const [id, s] of sessions) {
63
+ if (now - s.lastSeen > timeoutMs)
64
+ sessions.delete(id);
65
+ }
66
+ }
67
+ const gcTimer = setInterval(gc, 30000);
68
+ return {
69
+ create() {
70
+ const id = crypto.randomUUID();
71
+ sessions.set(id, { id, createdAt: Date.now(), lastSeen: Date.now() });
72
+ return id;
73
+ },
74
+ touch(id) {
75
+ const s = sessions.get(id);
76
+ if (!s)
77
+ return false;
78
+ s.lastSeen = Date.now();
79
+ return true;
80
+ },
81
+ delete(id) {
82
+ sessions.delete(id);
83
+ },
84
+ stop() {
85
+ clearInterval(gcTimer);
86
+ }
87
+ };
88
+ }
89
+ function buildCorsHeaders(cors, requestOrigin) {
90
+ if (!cors)
91
+ return {};
92
+ if (cors === true) {
93
+ return {
94
+ "Access-Control-Allow-Origin": requestOrigin ?? "*",
95
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
96
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key, Mcp-Session-Id",
97
+ "Access-Control-Allow-Credentials": "true"
98
+ };
99
+ }
100
+ const origins = Array.isArray(cors.origins) ? cors.origins : cors.origins ? [cors.origins] : ["*"];
101
+ const allowedOrigin = requestOrigin && origins.includes(requestOrigin) ? requestOrigin : origins[0] ?? "*";
102
+ return {
103
+ "Access-Control-Allow-Origin": allowedOrigin,
104
+ "Access-Control-Allow-Methods": (cors.methods ?? ["GET", "POST", "DELETE", "OPTIONS"]).join(", "),
105
+ "Access-Control-Allow-Headers": (cors.headers ?? [
106
+ "Content-Type",
107
+ "Authorization",
108
+ "X-API-Key",
109
+ "Mcp-Session-Id"
110
+ ]).join(", "),
111
+ "Access-Control-Allow-Credentials": String(cors.credentials ?? true)
112
+ };
113
+ }
114
+ function getRequestHeaders(headers) {
115
+ return Object.fromEntries(Array.from(headers.entries()).map(([key, value]) => [
116
+ key.toLowerCase(),
117
+ value
118
+ ]));
119
+ }
120
+ async function handleJsonRpc(body, tools, runner, requestMeta, serverInfo, sessionId) {
121
+ const { id, method, params } = body;
122
+ if (method === "initialize") {
123
+ return {
124
+ jsonrpc: "2.0",
125
+ id,
126
+ result: {
127
+ protocolVersion: "2024-11-05",
128
+ capabilities: { tools: { listChanged: false } },
129
+ serverInfo,
130
+ sessionId
131
+ }
132
+ };
133
+ }
134
+ if (method === "ping") {
135
+ return { jsonrpc: "2.0", id, result: {} };
136
+ }
137
+ if (method === "tools/list") {
138
+ return {
139
+ jsonrpc: "2.0",
140
+ id,
141
+ result: {
142
+ tools: Array.from(tools.values()).map((t) => ({
143
+ name: t.name,
144
+ description: t.description ?? "",
145
+ inputSchema: t.inputSchema,
146
+ ...t.annotations ? { annotations: t.annotations } : {}
147
+ }))
148
+ }
149
+ };
150
+ }
151
+ if (method === "tools/call") {
152
+ const p = params;
153
+ const toolName = p?.name;
154
+ if (!toolName || !tools.has(toolName)) {
155
+ return {
156
+ jsonrpc: "2.0",
157
+ id,
158
+ error: {
159
+ code: -32602,
160
+ message: `Unknown tool: ${toolName ?? "(none)"}`
161
+ }
162
+ };
163
+ }
164
+ try {
165
+ const result = await runner(toolName, p?.arguments ?? {}, requestMeta);
166
+ return {
167
+ jsonrpc: "2.0",
168
+ id,
169
+ result: {
170
+ content: [{ type: "text", text: JSON.stringify(result) }],
171
+ isError: false
172
+ }
173
+ };
174
+ } catch (err) {
175
+ return {
176
+ jsonrpc: "2.0",
177
+ id,
178
+ result: {
179
+ content: [
180
+ {
181
+ type: "text",
182
+ text: String(err instanceof Error ? err.message : err)
183
+ }
184
+ ],
185
+ isError: true
186
+ }
187
+ };
188
+ }
189
+ }
190
+ return {
191
+ jsonrpc: "2.0",
192
+ id,
193
+ error: { code: -32601, message: `Method not found: ${method}` }
194
+ };
195
+ }
196
+ function startHttpTransport(tools, runner, opts, serverInfo) {
197
+ const port = Number(opts.port ?? 3000);
198
+ const hostname = opts.hostname ?? "localhost";
199
+ const mcpPath = opts.path ?? "/mcp";
200
+ const sessionTimeout = opts.sessionTimeout ?? 30000;
201
+ const maxBodySize = opts.maxBodySize ?? 4 * 1024 * 1024;
202
+ const sessions = createSessionStore(sessionTimeout);
203
+ const sseClients = new Map;
204
+ const server = Bun.serve({
205
+ port,
206
+ hostname,
207
+ ...opts.tls ? { tls: opts.tls } : {},
208
+ async fetch(req, server2) {
209
+ const url2 = new URL(req.url);
210
+ const origin = req.headers.get("origin");
211
+ const corsHeaders = buildCorsHeaders(opts.cors, origin);
212
+ if (req.method === "OPTIONS") {
213
+ return new Response(null, { status: 204, headers: corsHeaders });
214
+ }
215
+ if (req.method === "GET" && url2.pathname === `${mcpPath}/health`) {
216
+ return Response.json({ ok: true, tools: tools.size }, { headers: corsHeaders });
217
+ }
218
+ if (req.method === "GET" && url2.pathname === `${mcpPath}/schema`) {
219
+ const schema = {
220
+ openapi: "3.1.0",
221
+ info: {
222
+ title: `${serverInfo.name} MCP server`,
223
+ version: serverInfo.version
224
+ },
225
+ paths: Object.fromEntries(Array.from(tools.values()).map((t) => [
226
+ `/tools/${t.name}`,
227
+ {
228
+ post: {
229
+ summary: t.description,
230
+ requestBody: {
231
+ content: { "application/json": { schema: t.inputSchema } }
232
+ }
233
+ }
234
+ }
235
+ ]))
236
+ };
237
+ return Response.json(schema, { headers: corsHeaders });
238
+ }
239
+ if (req.method === "GET" && url2.pathname === mcpPath) {
240
+ let sessionId = req.headers.get("mcp-session-id") ?? "";
241
+ if (!sessions.touch(sessionId)) {
242
+ sessionId = sessions.create();
243
+ }
244
+ const stream = new ReadableStream({
245
+ start(controller) {
246
+ sseClients.set(sessionId, controller);
247
+ const event = `event: endpoint
248
+ data: ${JSON.stringify({
249
+ uri: `${url2.origin}${mcpPath}`,
250
+ sessionId
251
+ })}
252
+
253
+ `;
254
+ controller.enqueue(new TextEncoder().encode(event));
255
+ },
256
+ cancel() {
257
+ sseClients.delete(sessionId);
258
+ sessions.delete(sessionId);
259
+ }
260
+ });
261
+ return new Response(stream, {
262
+ headers: {
263
+ ...corsHeaders,
264
+ "Content-Type": "text/event-stream",
265
+ "Cache-Control": "no-cache",
266
+ Connection: "keep-alive",
267
+ "Mcp-Session-Id": sessionId
268
+ }
269
+ });
270
+ }
271
+ if (req.method === "POST" && url2.pathname === mcpPath) {
272
+ const contentLength = Number(req.headers.get("content-length") ?? 0);
273
+ if (contentLength > maxBodySize) {
274
+ return new Response("Payload Too Large", {
275
+ status: 413,
276
+ headers: corsHeaders
277
+ });
278
+ }
279
+ let body;
280
+ try {
281
+ body = await req.json();
282
+ } catch {
283
+ return Response.json({
284
+ jsonrpc: "2.0",
285
+ id: null,
286
+ error: { code: -32700, message: "Parse error" }
287
+ }, { status: 400, headers: corsHeaders });
288
+ }
289
+ let sessionId = req.headers.get("mcp-session-id") ?? "";
290
+ if (!sessions.touch(sessionId)) {
291
+ sessionId = sessions.create();
292
+ }
293
+ const result = await handleJsonRpc(body, tools, runner, {
294
+ headers: getRequestHeaders(req.headers),
295
+ ip: server2.requestIP(req)?.address,
296
+ method: req.method,
297
+ raw: req,
298
+ sessionId,
299
+ transport: "http",
300
+ url: req.url
301
+ }, serverInfo, sessionId);
302
+ return Response.json(result, {
303
+ headers: {
304
+ ...corsHeaders,
305
+ "Mcp-Session-Id": sessionId
306
+ }
307
+ });
308
+ }
309
+ if (req.method === "DELETE" && url2.pathname === mcpPath) {
310
+ const sessionId = req.headers.get("mcp-session-id") ?? "";
311
+ const ctrl = sseClients.get(sessionId);
312
+ if (ctrl) {
313
+ try {
314
+ ctrl.close();
315
+ } catch {}
316
+ sseClients.delete(sessionId);
317
+ }
318
+ sessions.delete(sessionId);
319
+ return Response.json({ ok: true, sessionId: sessionId || null, terminated: true }, { headers: corsHeaders });
320
+ }
321
+ return new Response("Not Found", { status: 404, headers: corsHeaders });
322
+ },
323
+ error(err) {
324
+ console.error("[redop] server error:", err);
325
+ return new Response("Internal Server Error", { status: 500 });
326
+ }
327
+ });
328
+ const url = `http${opts.tls ? "s" : ""}://${hostname}:${port}${mcpPath}`;
329
+ opts.onListen?.({ hostname, port, url });
330
+ return {
331
+ stop() {
332
+ sessions.stop();
333
+ server.stop();
334
+ },
335
+ broadcast(sessionId, event, data) {
336
+ const ctrl = sseClients.get(sessionId);
337
+ if (ctrl) {
338
+ const msg = `event: ${event}
339
+ data: ${JSON.stringify(data)}
340
+
341
+ `;
342
+ ctrl.enqueue(new TextEncoder().encode(msg));
343
+ }
344
+ }
345
+ };
346
+ }
347
+
348
+ // src/transports/stdio.ts
349
+ function buildToolList(tools) {
350
+ return Array.from(tools.values()).map((t) => ({
351
+ name: t.name,
352
+ description: t.description ?? "",
353
+ inputSchema: t.inputSchema,
354
+ ...t.annotations ? { annotations: t.annotations } : {}
355
+ }));
356
+ }
357
+ async function handleMessage(msg, tools, runner, serverInfo) {
358
+ const { id, method, params } = msg;
359
+ const respond = (result) => process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + `
360
+ `);
361
+ const error = (code, message) => process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + `
362
+ `);
363
+ if (method === "initialize") {
364
+ respond({
365
+ protocolVersion: "2024-11-05",
366
+ capabilities: { tools: { listChanged: false } },
367
+ serverInfo
368
+ });
369
+ return;
370
+ }
371
+ if (method === "notifications/initialized") {
372
+ return;
373
+ }
374
+ if (method === "ping") {
375
+ respond({});
376
+ return;
377
+ }
378
+ if (method === "tools/list") {
379
+ respond({ tools: buildToolList(tools) });
380
+ return;
381
+ }
382
+ if (method === "tools/call") {
383
+ const p = params;
384
+ const toolName = p?.name;
385
+ if (!toolName || !tools.has(toolName)) {
386
+ error(-32602, `Unknown tool: ${toolName ?? "(none)"}`);
387
+ return;
388
+ }
389
+ try {
390
+ const result = await runner(toolName, p?.arguments ?? {}, {
391
+ headers: {},
392
+ transport: "stdio"
393
+ });
394
+ respond({
395
+ content: [{ type: "text", text: JSON.stringify(result) }],
396
+ isError: false
397
+ });
398
+ } catch (err) {
399
+ respond({
400
+ content: [
401
+ { type: "text", text: String(err instanceof Error ? err.message : err) }
402
+ ],
403
+ isError: true
404
+ });
405
+ }
406
+ return;
407
+ }
408
+ error(-32601, `Method not found: ${method}`);
409
+ }
410
+ function startStdioTransport(tools, runner, serverInfo) {
411
+ process.stdin.setEncoding("utf8");
412
+ let buffer = "";
413
+ process.stdin.on("data", async (chunk) => {
414
+ buffer += chunk;
415
+ const lines = buffer.split(`
416
+ `);
417
+ buffer = lines.pop() ?? "";
418
+ for (const line of lines) {
419
+ const trimmed = line.trim();
420
+ if (!trimmed)
421
+ continue;
422
+ let msg;
423
+ try {
424
+ msg = JSON.parse(trimmed);
425
+ } catch {
426
+ process.stdout.write(JSON.stringify({
427
+ jsonrpc: "2.0",
428
+ id: null,
429
+ error: { code: -32700, message: "Parse error" }
430
+ }) + `
431
+ `);
432
+ continue;
433
+ }
434
+ await handleMessage(msg, tools, runner, serverInfo);
435
+ }
436
+ });
437
+ process.stdin.on("end", () => process.exit(0));
438
+ process.stdin.resume();
439
+ }
440
+
441
+ // src/redop.ts
442
+ var DEFAULT_REQUEST_META = {
443
+ headers: {},
444
+ transport: "stdio"
445
+ };
446
+ var DEFAULT_SERVER_INFO = {
447
+ name: "redop",
448
+ version: "0.1.0"
449
+ };
450
+
451
+ class Redop {
452
+ _hooks = {
453
+ before: [],
454
+ after: [],
455
+ error: [],
456
+ transform: [],
457
+ mapResponse: []
458
+ };
459
+ _tools = new Map;
460
+ _middlewares = [];
461
+ _inputParsers = new Map;
462
+ _schemaAdapter;
463
+ _serverInfo = { ...DEFAULT_SERVER_INFO };
464
+ _prefix = "";
465
+ constructor(options = {}) {
466
+ const serverInfo = {
467
+ ...DEFAULT_SERVER_INFO,
468
+ ...options.name ? { name: options.name } : {},
469
+ ...options.version ? { version: options.version } : {}
470
+ };
471
+ this._serverInfo = serverInfo;
472
+ if (options?.schemaAdapter) {
473
+ this._schemaAdapter = options.schemaAdapter;
474
+ }
475
+ }
476
+ onBeforeHandle(hook) {
477
+ this._hooks.before.push(hook);
478
+ return this;
479
+ }
480
+ onAfterHandle(hook) {
481
+ this._hooks.after.push(hook);
482
+ return this;
483
+ }
484
+ onError(hook) {
485
+ this._hooks.error.push(hook);
486
+ return this;
487
+ }
488
+ onTransform(hook) {
489
+ this._hooks.transform.push(hook);
490
+ return this;
491
+ }
492
+ mapResponse(hook) {
493
+ this._hooks.mapResponse.push(hook);
494
+ return this;
495
+ }
496
+ middleware(mw) {
497
+ this._middlewares.push(mw);
498
+ return this;
499
+ }
500
+ tool(name, def) {
501
+ const fullName = this._prefix ? `${this._prefix}_${name}` : name;
502
+ let inputSchema = {
503
+ type: "object",
504
+ properties: {}
505
+ };
506
+ if (def.input) {
507
+ const adapter = this._schemaAdapter ?? detectAdapter(def.input);
508
+ inputSchema = adapter.toJsonSchema(def.input);
509
+ this._inputParsers.set(fullName, (input) => adapter.parse(def.input, input));
510
+ }
511
+ this._tools.set(fullName, {
512
+ name: fullName,
513
+ before: def.before,
514
+ after: def.after,
515
+ description: def.description,
516
+ annotations: def.annotations,
517
+ inputSchema,
518
+ handler: def.handler
519
+ });
520
+ return this;
521
+ }
522
+ group(prefix, callback) {
523
+ const scoped = new Redop({
524
+ name: this._serverInfo.name,
525
+ schemaAdapter: this._schemaAdapter,
526
+ version: this._serverInfo.version
527
+ });
528
+ scoped._prefix = this._prefix ? `${this._prefix}_${prefix}` : prefix;
529
+ scoped._hooks = this._hooks;
530
+ scoped._middlewares = this._middlewares;
531
+ callback(scoped);
532
+ for (const [name, tool] of scoped._tools) {
533
+ this._tools.set(name, tool);
534
+ }
535
+ for (const [name, parser] of scoped._inputParsers) {
536
+ this._inputParsers.set(name, parser);
537
+ }
538
+ return this;
539
+ }
540
+ use(plugin) {
541
+ this._hooks.before.push(...plugin._hooks.before);
542
+ this._hooks.after.push(...plugin._hooks.after);
543
+ this._hooks.error.push(...plugin._hooks.error);
544
+ this._hooks.transform.push(...plugin._hooks.transform);
545
+ this._hooks.mapResponse.push(...plugin._hooks.mapResponse);
546
+ this._middlewares.push(...plugin._middlewares);
547
+ for (const [name, tool] of plugin._tools) {
548
+ this._tools.set(name, tool);
549
+ }
550
+ for (const [name, parser] of plugin._inputParsers) {
551
+ this._inputParsers.set(name, parser);
552
+ }
553
+ return this;
554
+ }
555
+ async _runTool(toolName, rawArgs, request = DEFAULT_REQUEST_META) {
556
+ const tool = this._tools.get(toolName);
557
+ if (!tool)
558
+ throw new Error(`Unknown tool: ${toolName}`);
559
+ const ctx = {
560
+ headers: request.headers,
561
+ requestId: crypto.randomUUID(),
562
+ sessionId: request.sessionId,
563
+ tool: toolName,
564
+ transport: request.transport,
565
+ rawParams: rawArgs
566
+ };
567
+ let params = { ...rawArgs };
568
+ for (const hook of this._hooks.transform) {
569
+ const out = await hook({ tool: toolName, params, ctx, request });
570
+ if (out && typeof out === "object") {
571
+ params = out;
572
+ }
573
+ }
574
+ let input = params;
575
+ const parser = this._inputParsers.get(toolName);
576
+ if (parser) {
577
+ try {
578
+ input = await parser(params);
579
+ } catch (err) {
580
+ const validationError = new Error(`Validation failed for tool "${toolName}": ${err instanceof Error ? err.message : String(err)}`);
581
+ validationError.cause = err;
582
+ if (typeof err === "object" && err !== null && "issues" in err) {
583
+ validationError.issues = err.issues;
584
+ }
585
+ throw validationError;
586
+ }
587
+ }
588
+ const handlerEvent = {
589
+ ctx,
590
+ input,
591
+ request,
592
+ tool: toolName
593
+ };
594
+ try {
595
+ for (const hook of this._hooks.before) {
596
+ await hook({
597
+ tool: toolName,
598
+ ctx,
599
+ input,
600
+ params: input,
601
+ request
602
+ });
603
+ }
604
+ if (tool.before) {
605
+ await tool.before(handlerEvent);
606
+ }
607
+ const dispatch = async (index) => {
608
+ if (index >= this._middlewares.length) {
609
+ return tool.handler(handlerEvent);
610
+ }
611
+ const mw = this._middlewares[index];
612
+ if (!mw) {
613
+ return tool.handler(handlerEvent);
614
+ }
615
+ return mw({
616
+ ...handlerEvent,
617
+ next: () => dispatch(index + 1)
618
+ });
619
+ };
620
+ let result = await dispatch(0);
621
+ if (tool.after) {
622
+ await tool.after({
623
+ ...handlerEvent,
624
+ result
625
+ });
626
+ }
627
+ for (const hook of this._hooks.after) {
628
+ await hook({
629
+ tool: toolName,
630
+ ctx,
631
+ input,
632
+ params: input,
633
+ request,
634
+ result
635
+ });
636
+ }
637
+ for (const hook of this._hooks.mapResponse) {
638
+ result = await hook(result, toolName);
639
+ }
640
+ return result;
641
+ } catch (err) {
642
+ for (const hook of this._hooks.error) {
643
+ await hook({
644
+ tool: toolName,
645
+ ctx,
646
+ error: err,
647
+ input,
648
+ params: input,
649
+ request
650
+ });
651
+ }
652
+ throw err;
653
+ }
654
+ }
655
+ listen(opts = {}) {
656
+ const runner = (name, args, requestMeta) => this._runTool(name, args, requestMeta);
657
+ const transport = opts.transport ?? (opts.port ? "http" : "stdio");
658
+ if (transport === "stdio") {
659
+ startStdioTransport(this._tools, runner, this._serverInfo);
660
+ return this;
661
+ }
662
+ if (transport === "http") {
663
+ startHttpTransport(this._tools, runner, opts, this._serverInfo);
664
+ return this;
665
+ }
666
+ throw new Error(`[redop] Unknown transport: ${transport}`);
667
+ }
668
+ get toolNames() {
669
+ return Array.from(this._tools.keys());
670
+ }
671
+ get serverInfo() {
672
+ return { ...this._serverInfo };
673
+ }
674
+ getTool(name) {
675
+ return this._tools.get(name);
676
+ }
677
+ }
678
+ function middleware(fn) {
679
+ return new Redop().middleware(fn);
680
+ }
681
+ function definePlugin(definition) {
682
+ const factory = (options) => definition.setup(options);
683
+ const meta = {
684
+ name: definition.name,
685
+ version: definition.version,
686
+ ...definition.description ? { description: definition.description } : {}
687
+ };
688
+ factory.meta = meta;
689
+ return factory;
690
+ }
691
+
692
+ // src/plugins/index.ts
693
+ var LOG_LEVELS = {
694
+ debug: 0,
695
+ info: 1,
696
+ warn: 2,
697
+ error: 3
698
+ };
699
+ function logger(opts = {}) {
700
+ const minLevel = LOG_LEVELS[opts.level ?? "info"];
701
+ const write = opts.write ?? ((e) => console.log(JSON.stringify(e)));
702
+ const log = (level, data) => {
703
+ if (LOG_LEVELS[level] >= minLevel) {
704
+ write({ ts: new Date().toISOString(), level, ...data });
705
+ }
706
+ };
707
+ return new Redop().onBeforeHandle(({ tool, ctx, request }) => {
708
+ log("info", {
709
+ event: "tool.start",
710
+ tool,
711
+ requestId: ctx.requestId,
712
+ transport: request.transport
713
+ });
714
+ }).onAfterHandle(({ tool, ctx }) => {
715
+ const ms = ctx.startedAt ? performance.now() - ctx.startedAt : undefined;
716
+ log("info", {
717
+ event: "tool.end",
718
+ tool,
719
+ requestId: ctx.requestId,
720
+ ...ms != null ? { ms: +ms.toFixed(2) } : {}
721
+ });
722
+ }).onError(({ tool, error, ctx }) => {
723
+ log("error", {
724
+ event: "tool.error",
725
+ tool,
726
+ requestId: ctx.requestId,
727
+ error: error instanceof Error ? error.message : String(error)
728
+ });
729
+ });
730
+ }
731
+ function analytics(opts = {}) {
732
+ const { sink = "console", apiKey } = opts;
733
+ async function emit(event) {
734
+ if (typeof sink === "function") {
735
+ await sink(event);
736
+ return;
737
+ }
738
+ if (sink === "console") {
739
+ console.log("[redop:analytics]", event);
740
+ return;
741
+ }
742
+ if (sink === "posthog" && apiKey) {
743
+ fetch("https://app.posthog.com/capture/", {
744
+ method: "POST",
745
+ headers: { "Content-Type": "application/json" },
746
+ body: JSON.stringify({
747
+ api_key: apiKey,
748
+ event: "redop_tool_call",
749
+ distinct_id: event.requestId,
750
+ properties: event
751
+ })
752
+ }).catch(() => {});
753
+ }
754
+ }
755
+ return new Redop().onBeforeHandle(({ ctx }) => {
756
+ ctx.startedAt = performance.now();
757
+ ctx.analyticsSuccess = true;
758
+ }).onError(({ ctx }) => {
759
+ ctx.analyticsSuccess = false;
760
+ }).onAfterHandle(({ tool, ctx }) => {
761
+ const startedAt = ctx.startedAt;
762
+ const durationMs = startedAt != null ? +(performance.now() - startedAt).toFixed(2) : 0;
763
+ emit({
764
+ tool,
765
+ durationMs,
766
+ success: ctx.analyticsSuccess ?? true,
767
+ requestId: ctx.requestId
768
+ });
769
+ });
770
+ }
771
+ function apiKey(opts = {}) {
772
+ return createHeaderAuthPlugin({
773
+ ...opts,
774
+ ctxKey: opts.ctxKey ?? "apiKey",
775
+ headerName: opts.headerName ?? "x-api-key"
776
+ });
777
+ }
778
+ function bearer(opts = {}) {
779
+ const scheme = opts.scheme ?? "Bearer";
780
+ return createHeaderAuthPlugin({
781
+ ...opts,
782
+ ctxKey: opts.ctxKey ?? "token",
783
+ headerName: "authorization",
784
+ aliases: ["authToken"],
785
+ transform(value) {
786
+ const [providedScheme, ...rest] = value.trim().split(/\s+/);
787
+ if (!providedScheme || providedScheme.toLowerCase() !== scheme.toLowerCase()) {
788
+ throw new Error(`Unauthorized: expected ${scheme} token`);
789
+ }
790
+ const token = rest.join(" ").trim();
791
+ if (!token) {
792
+ throw new Error(`Unauthorized: missing ${scheme} token`);
793
+ }
794
+ return token;
795
+ }
796
+ });
797
+ }
798
+ function createHeaderAuthPlugin(opts) {
799
+ return middleware(async ({ ctx, request, input, tool, next }) => {
800
+ if (request.transport !== "http")
801
+ return next();
802
+ const headerName = (opts.headerName ?? "authorization").toLowerCase();
803
+ const headerValue = request.headers[headerName];
804
+ if (!opts.secret && !opts.validate) {
805
+ throw new Error(`[redop] ${headerName} auth requires either a secret or validate()`);
806
+ }
807
+ if (!headerValue) {
808
+ if (opts.required ?? true) {
809
+ throw new Error(`Unauthorized: missing ${headerName} header`);
810
+ }
811
+ return next();
812
+ }
813
+ const token = opts.transform ? opts.transform(headerValue) : headerValue.trim();
814
+ const valid = opts.validate ? await opts.validate(token, { ctx, input, request, tool }) : token === opts.secret;
815
+ if (!valid) {
816
+ throw new Error(`Unauthorized: invalid ${headerName}`);
817
+ }
818
+ ctx[opts.ctxKey ?? "auth"] = token;
819
+ for (const alias of opts.aliases ?? []) {
820
+ ctx[alias] = token;
821
+ }
822
+ return next();
823
+ });
824
+ }
825
+ function parseWindow(w) {
826
+ if (typeof w === "number")
827
+ return w;
828
+ const match = w.match(/^(\d+)(ms|s|m|h|d)$/);
829
+ if (!match)
830
+ return 60000;
831
+ const n = match[1];
832
+ const unit = match[2];
833
+ if (!n || !unit)
834
+ return 60000;
835
+ const multipliers = {
836
+ ms: 1,
837
+ s: 1000,
838
+ m: 60000,
839
+ h: 3600000,
840
+ d: 86400000
841
+ };
842
+ return parseInt(n) * (multipliers[unit] ?? 1);
843
+ }
844
+ function defaultRateLimitKey(event) {
845
+ return event.request.ip ?? event.request.headers["x-forwarded-for"]?.split(",")[0]?.trim() ?? event.request.sessionId ?? event.ctx.requestId.slice(0, 8);
846
+ }
847
+ function rateLimit(opts = {}) {
848
+ const max = opts.max ?? 60;
849
+ const windowMs = parseWindow(opts.window ?? "1m");
850
+ const buckets = new Map;
851
+ return middleware(async (event) => {
852
+ const key = opts.keyBy ? opts.keyBy(event) : defaultRateLimitKey(event);
853
+ const now = Date.now();
854
+ const timestamps = (buckets.get(key) ?? []).filter((ts) => now - ts < windowMs);
855
+ if (timestamps.length >= max) {
856
+ throw new Error(`Rate limit exceeded: ${max} calls per ${windowMs}ms`);
857
+ }
858
+ timestamps.push(now);
859
+ buckets.set(key, timestamps);
860
+ return event.next();
861
+ });
862
+ }
863
+ function cache(opts = {}) {
864
+ const ttl = opts.ttl ?? 60000;
865
+ const allowedTools = opts.tools ? new Set(opts.tools) : null;
866
+ const store = new Map;
867
+ function hashKey(tool, input) {
868
+ return `${tool}:${JSON.stringify(input)}`;
869
+ }
870
+ return middleware(async ({ tool, input, next }) => {
871
+ if (allowedTools && !allowedTools.has(tool)) {
872
+ return next();
873
+ }
874
+ const key = hashKey(tool, input);
875
+ const entry = store.get(key);
876
+ if (entry && Date.now() < entry.expiresAt) {
877
+ return entry.result;
878
+ }
879
+ const result = await next();
880
+ store.set(key, { result, expiresAt: Date.now() + ttl });
881
+ return result;
882
+ });
883
+ }
884
+ // src/types.ts
885
+ var McpErrorCode;
886
+ ((McpErrorCode2) => {
887
+ McpErrorCode2[McpErrorCode2["ParseError"] = -32700] = "ParseError";
888
+ McpErrorCode2[McpErrorCode2["InvalidRequest"] = -32600] = "InvalidRequest";
889
+ McpErrorCode2[McpErrorCode2["MethodNotFound"] = -32601] = "MethodNotFound";
890
+ McpErrorCode2[McpErrorCode2["InvalidParams"] = -32602] = "InvalidParams";
891
+ McpErrorCode2[McpErrorCode2["InternalError"] = -32603] = "InternalError";
892
+ McpErrorCode2[McpErrorCode2["ToolNotFound"] = -32000] = "ToolNotFound";
893
+ McpErrorCode2[McpErrorCode2["ValidationFailed"] = -32001] = "ValidationFailed";
894
+ McpErrorCode2[McpErrorCode2["Unauthorized"] = -32002] = "Unauthorized";
895
+ McpErrorCode2[McpErrorCode2["RateLimited"] = -32003] = "RateLimited";
896
+ McpErrorCode2[McpErrorCode2["Timeout"] = -32004] = "Timeout";
897
+ })(McpErrorCode ||= {});
898
+
899
+ class McpError extends Error {
900
+ code;
901
+ data;
902
+ constructor(code, message, data) {
903
+ super(message);
904
+ this.code = code;
905
+ this.data = data;
906
+ this.name = "McpError";
907
+ }
908
+ }
909
+ export {
910
+ zodAdapter,
911
+ standardSchemaAdapter,
912
+ rateLimit,
913
+ middleware,
914
+ logger,
915
+ jsonSchemaAdapter,
916
+ detectAdapter,
917
+ definePlugin,
918
+ cache,
919
+ bearer,
920
+ apiKey,
921
+ analytics,
922
+ Redop,
923
+ McpErrorCode,
924
+ McpError
925
+ };