mcpose 2.1.1 → 3.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.
Files changed (101) hide show
  1. package/README.md +461 -62
  2. package/dist/backendClient.d.ts +1 -1
  3. package/dist/backendClient.d.ts.map +1 -1
  4. package/dist/backendClient.js +6 -1
  5. package/dist/backendClient.js.map +1 -1
  6. package/dist/core.d.ts +309 -35
  7. package/dist/core.d.ts.map +1 -1
  8. package/dist/core.js +1023 -186
  9. package/dist/core.js.map +1 -1
  10. package/dist/delegation.d.ts +89 -0
  11. package/dist/delegation.d.ts.map +1 -0
  12. package/dist/delegation.js +175 -0
  13. package/dist/delegation.js.map +1 -0
  14. package/dist/eventStore.d.ts +12 -0
  15. package/dist/eventStore.d.ts.map +1 -1
  16. package/dist/eventStore.js +43 -5
  17. package/dist/eventStore.js.map +1 -1
  18. package/dist/hiddenTools.d.ts +42 -0
  19. package/dist/hiddenTools.d.ts.map +1 -0
  20. package/dist/hiddenTools.js +53 -0
  21. package/dist/hiddenTools.js.map +1 -0
  22. package/dist/index.d.ts +15 -6
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +7 -3
  25. package/dist/index.js.map +1 -1
  26. package/dist/mesh.d.ts +82 -0
  27. package/dist/mesh.d.ts.map +1 -0
  28. package/dist/mesh.js +141 -0
  29. package/dist/mesh.js.map +1 -0
  30. package/dist/middleware.d.ts +13 -0
  31. package/dist/middleware.d.ts.map +1 -1
  32. package/dist/middleware.js +27 -3
  33. package/dist/middleware.js.map +1 -1
  34. package/dist/proxyContext.d.ts +65 -5
  35. package/dist/proxyContext.d.ts.map +1 -1
  36. package/dist/proxyContext.js +39 -4
  37. package/dist/proxyContext.js.map +1 -1
  38. package/dist/rejection.d.ts +7 -1
  39. package/dist/rejection.d.ts.map +1 -1
  40. package/dist/rejection.js +24 -1
  41. package/dist/rejection.js.map +1 -1
  42. package/dist/sanitizeCatalog.d.ts +37 -0
  43. package/dist/sanitizeCatalog.d.ts.map +1 -0
  44. package/dist/sanitizeCatalog.js +92 -0
  45. package/dist/sanitizeCatalog.js.map +1 -0
  46. package/dist/sessionRegistry.d.ts +79 -0
  47. package/dist/sessionRegistry.d.ts.map +1 -0
  48. package/dist/sessionRegistry.js +83 -0
  49. package/dist/sessionRegistry.js.map +1 -0
  50. package/dist/telemetry.d.ts +29 -1
  51. package/dist/telemetry.d.ts.map +1 -1
  52. package/dist/testing.d.ts +18 -2
  53. package/dist/testing.d.ts.map +1 -1
  54. package/dist/testing.js +30 -8
  55. package/dist/testing.js.map +1 -1
  56. package/dist/version.d.ts +7 -0
  57. package/dist/version.d.ts.map +1 -0
  58. package/dist/version.js +7 -0
  59. package/dist/version.js.map +1 -0
  60. package/package.json +22 -8
  61. package/src/__tests__/_helpers.ts +64 -0
  62. package/src/__tests__/backendClient.test.ts +198 -0
  63. package/src/__tests__/core.delegation.test.ts +964 -0
  64. package/src/__tests__/core.edge.test.ts +314 -0
  65. package/src/__tests__/core.hiddenTools.predicate.test.ts +236 -0
  66. package/src/__tests__/core.localTools.test.ts +314 -0
  67. package/src/__tests__/core.mesh.test.ts +1182 -0
  68. package/src/__tests__/core.stripRequestMeta.test.ts +226 -0
  69. package/src/__tests__/core.stripResultMeta.test.ts +217 -0
  70. package/src/__tests__/core.test.ts +1105 -0
  71. package/src/__tests__/core.toolCallTelemetry.test.ts +157 -0
  72. package/src/__tests__/eventStore.test.ts +141 -0
  73. package/src/__tests__/hasToolContent.test.ts +63 -0
  74. package/src/__tests__/mapToolResult.test.ts +101 -0
  75. package/src/__tests__/middleware.edge.test.ts +126 -0
  76. package/src/__tests__/middleware.test.ts +233 -0
  77. package/src/__tests__/proxyContext.test.ts +140 -0
  78. package/src/__tests__/sanitizeCatalog.test.ts +380 -0
  79. package/src/__tests__/startHttpProxy.lifecycle.test.ts +675 -0
  80. package/src/__tests__/startHttpProxy.registry.test.ts +409 -0
  81. package/src/__tests__/startHttpProxy.replay.test.ts +116 -0
  82. package/src/__tests__/startHttpProxy.security.test.ts +288 -0
  83. package/src/__tests__/startHttpProxy.test.ts +1065 -0
  84. package/src/__tests__/testing.test.ts +250 -0
  85. package/src/__tests__/version.test.ts +12 -0
  86. package/src/backendClient.ts +83 -0
  87. package/src/core.ts +2129 -0
  88. package/src/delegation.ts +224 -0
  89. package/src/eventStore.ts +106 -0
  90. package/src/hiddenTools.ts +84 -0
  91. package/src/identity.ts +15 -0
  92. package/src/index.ts +63 -0
  93. package/src/mesh.ts +204 -0
  94. package/src/middleware.ts +101 -0
  95. package/src/proxyContext.ts +108 -0
  96. package/src/rejection.ts +62 -0
  97. package/src/sanitizeCatalog.ts +126 -0
  98. package/src/sessionRegistry.ts +142 -0
  99. package/src/telemetry.ts +46 -0
  100. package/src/testing.ts +174 -0
  101. package/src/version.ts +6 -0
@@ -0,0 +1,233 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { compose, pipe, type Middleware } from '../middleware.js';
3
+ import type { ProxyContext } from '../proxyContext.js';
4
+
5
+ type Req = { value: number };
6
+ type Res = { result: number };
7
+
8
+ const identity: Middleware<Req, Res> = (req, next) => next(req);
9
+
10
+ describe('compose()', () => {
11
+ it('calls innermost next when middlewares array is empty', async () => {
12
+ const pipeline = compose<Req, Res>([]);
13
+ const result = await pipeline({ value: 1 }, async (r) => ({
14
+ result: r.value * 10,
15
+ }));
16
+ expect(result).toEqual({ result: 10 });
17
+ });
18
+
19
+ it('passes through a single identity middleware', async () => {
20
+ const pipeline = compose([identity]);
21
+ const result = await pipeline({ value: 3 }, async (r) => ({
22
+ result: r.value,
23
+ }));
24
+ expect(result).toEqual({ result: 3 });
25
+ });
26
+
27
+ it('passes ProxyContext through the entire middleware chain unchanged', async () => {
28
+ const context: ProxyContext = {
29
+ requestId: 'req-1',
30
+ transport: 'http',
31
+ sessionId: 'sess-1',
32
+ };
33
+ const seen: ProxyContext[] = [];
34
+
35
+ const outer: Middleware<Req, Res> = async (req, next, currentContext) => {
36
+ seen.push(currentContext);
37
+ return next(req);
38
+ };
39
+
40
+ const inner: Middleware<Req, Res> = async (req, next, currentContext) => {
41
+ seen.push(currentContext);
42
+ return next(req);
43
+ };
44
+
45
+ const pipeline = compose([outer, inner]);
46
+ await pipeline({ value: 1 }, async (r) => ({ result: r.value }), context);
47
+
48
+ expect(seen).toEqual([context, context]);
49
+ });
50
+
51
+ it('creates a default ProxyContext when one is omitted', async () => {
52
+ let seenContext: ProxyContext | undefined;
53
+
54
+ const capture: Middleware<Req, Res> = async (req, next, context) => {
55
+ seenContext = context;
56
+ return next(req);
57
+ };
58
+
59
+ const pipeline = compose([capture]);
60
+ await pipeline({ value: 2 }, async (r) => ({ result: r.value }));
61
+
62
+ expect(seenContext?.transport).toBe('stdio');
63
+ expect(seenContext?.requestId).toEqual(expect.any(String));
64
+ });
65
+
66
+ it('executes middlewares in order — outer first, inner last', async () => {
67
+ const order: string[] = [];
68
+
69
+ const outer: Middleware<Req, Res> = async (req, next) => {
70
+ order.push('outer-enter');
71
+ const res = await next(req);
72
+ order.push('outer-exit');
73
+ return res;
74
+ };
75
+
76
+ const inner: Middleware<Req, Res> = async (req, next) => {
77
+ order.push('inner-enter');
78
+ const res = await next(req);
79
+ order.push('inner-exit');
80
+ return res;
81
+ };
82
+
83
+ const pipeline = compose([outer, inner]);
84
+ await pipeline({ value: 0 }, async () => ({ result: 0 }));
85
+
86
+ expect(order).toEqual([
87
+ 'outer-enter',
88
+ 'inner-enter',
89
+ 'inner-exit',
90
+ 'outer-exit',
91
+ ]);
92
+ });
93
+
94
+ it('allows outer middleware to transform the response', async () => {
95
+ const doubler: Middleware<Req, Res> = async (req, next) => {
96
+ const res = await next(req);
97
+ return { result: res.result * 2 };
98
+ };
99
+
100
+ const pipeline = compose([doubler]);
101
+ const result = await pipeline({ value: 5 }, async (r) => ({
102
+ result: r.value,
103
+ }));
104
+ expect(result).toEqual({ result: 10 });
105
+ });
106
+
107
+ it('allows middleware to transform the request before passing on', async () => {
108
+ const incrementer: Middleware<Req, Res> = (req, next) =>
109
+ next({ value: req.value + 1 });
110
+
111
+ const pipeline = compose([incrementer]);
112
+ const result = await pipeline({ value: 4 }, async (r) => ({
113
+ result: r.value,
114
+ }));
115
+ expect(result).toEqual({ result: 5 });
116
+ });
117
+
118
+ it('chains multiple transformations correctly', async () => {
119
+ const add1: Middleware<Req, Res> = (req, next) =>
120
+ next({ value: req.value + 1 });
121
+ const mul2: Middleware<Req, Res> = (req, next) =>
122
+ next({ value: req.value * 2 });
123
+
124
+ // add1 outer: value = (original + 1) * 2
125
+ const pipeline = compose([add1, mul2]);
126
+ const result = await pipeline({ value: 3 }, async (r) => ({
127
+ result: r.value,
128
+ }));
129
+ expect(result).toEqual({ result: 8 }); // (3 + 1) * 2 = 8
130
+ });
131
+
132
+ it('throws if next() is called more than once', async () => {
133
+ const doubleNext: Middleware<Req, Res> = async (req, next) => {
134
+ await next(req);
135
+ return next(req); // second call — should throw
136
+ };
137
+
138
+ const pipeline = compose([doubleNext]);
139
+ await expect(
140
+ pipeline({ value: 0 }, async () => ({ result: 0 })),
141
+ ).rejects.toThrow('next() called multiple times');
142
+ });
143
+
144
+ it('propagates errors from the inner next', async () => {
145
+ const pipeline = compose([identity]);
146
+ await expect(
147
+ pipeline({ value: 0 }, async () => {
148
+ throw new Error('upstream error');
149
+ }),
150
+ ).rejects.toThrow('upstream error');
151
+ });
152
+
153
+ it('propagates errors thrown inside a middleware', async () => {
154
+ const failing: Middleware<Req, Res> = async () => {
155
+ throw new Error('middleware error');
156
+ };
157
+
158
+ const pipeline = compose([failing]);
159
+ await expect(
160
+ pipeline({ value: 0 }, async () => ({ result: 0 })),
161
+ ).rejects.toThrow('middleware error');
162
+ });
163
+ });
164
+
165
+ describe('pipe()', () => {
166
+ it('pipe([A, B]) is equivalent to compose([B, A])', async () => {
167
+ const add1: Middleware<Req, Res> = (req, next) =>
168
+ next({ value: req.value + 1 });
169
+ const mul2: Middleware<Req, Res> = (req, next) =>
170
+ next({ value: req.value * 2 });
171
+
172
+ // pipe([add1, mul2]) ≡ compose([mul2, add1])
173
+ // mul2 outer: value = (original * 2) + 1 = 7
174
+ const pipePipeline = pipe([add1, mul2]);
175
+ const composePipeline = compose([mul2, add1]);
176
+
177
+ const baseNext = async (r: Req): Promise<Res> => ({ result: r.value });
178
+ expect(await pipePipeline({ value: 3 }, baseNext)).toEqual({ result: 7 });
179
+ expect(await composePipeline({ value: 3 }, baseNext)).toEqual({
180
+ result: 7,
181
+ });
182
+ });
183
+
184
+ it('executes in data-flow order — first element processes the response first', async () => {
185
+ const order: string[] = [];
186
+
187
+ // first element in pipe() = innermost = processes response first
188
+ const innerMW: Middleware<Req, Res> = async (req, next) => {
189
+ order.push('inner-enter');
190
+ const res = await next(req);
191
+ order.push('inner-exit');
192
+ return res;
193
+ };
194
+
195
+ const outerMW: Middleware<Req, Res> = async (req, next) => {
196
+ order.push('outer-enter');
197
+ const res = await next(req);
198
+ order.push('outer-exit');
199
+ return res;
200
+ };
201
+
202
+ // pipe([innerMW, outerMW]) — outerMW wraps innerMW
203
+ const pipeline = pipe([innerMW, outerMW]);
204
+ await pipeline({ value: 0 }, async () => ({ result: 0 }));
205
+
206
+ expect(order).toEqual([
207
+ 'outer-enter',
208
+ 'inner-enter',
209
+ 'inner-exit',
210
+ 'outer-exit',
211
+ ]);
212
+ });
213
+
214
+ it('keeps the same ProxyContext while reversing response-processing order', async () => {
215
+ const context: ProxyContext = { requestId: 'req-2', transport: 'stdio' };
216
+ const seen: string[] = [];
217
+
218
+ const innerMW: Middleware<Req, Res> = async (req, next, currentContext) => {
219
+ seen.push(`inner:${currentContext.requestId}`);
220
+ return next(req);
221
+ };
222
+
223
+ const outerMW: Middleware<Req, Res> = async (req, next, currentContext) => {
224
+ seen.push(`outer:${currentContext.requestId}`);
225
+ return next(req);
226
+ };
227
+
228
+ const pipeline = pipe([innerMW, outerMW]);
229
+ await pipeline({ value: 1 }, async (r) => ({ result: r.value }), context);
230
+
231
+ expect(seen).toEqual(['outer:req-2', 'inner:req-2']);
232
+ });
233
+ });
@@ -0,0 +1,140 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { Identity } from '../identity.js';
3
+ import {
4
+ createProxyContext,
5
+ outboundDelegationChain,
6
+ } from '../proxyContext.js';
7
+
8
+ describe('createProxyContext()', () => {
9
+ it('defaults to stdio transport with a generated request ID', () => {
10
+ const context = createProxyContext();
11
+
12
+ expect(context.transport).toBe('stdio');
13
+ expect(context.requestId).toEqual(expect.any(String));
14
+ expect(context.requestId.length).toBeGreaterThan(0);
15
+ expect(context.sessionId).toBeUndefined();
16
+ expect(context.headers).toBeUndefined();
17
+ });
18
+
19
+ it('preserves supplied context fields', () => {
20
+ const headers = { authorization: 'Bearer token' };
21
+ const context = createProxyContext({
22
+ requestId: 'req-123',
23
+ transport: 'http',
24
+ sessionId: 'session-1',
25
+ headers,
26
+ });
27
+
28
+ expect(context).toEqual({
29
+ requestId: 'req-123',
30
+ transport: 'http',
31
+ sessionId: 'session-1',
32
+ headers,
33
+ });
34
+ });
35
+
36
+ it('carries a stamped policy decision through', () => {
37
+ const policy = { decision: 'allow', ruleId: 'r1' } as const;
38
+ expect(createProxyContext({ policy }).policy).toEqual(policy);
39
+ });
40
+
41
+ it('omits sessionId/headers/signal keys when they are not provided', () => {
42
+ const ctx = createProxyContext({ transport: 'http' });
43
+
44
+ expect('sessionId' in ctx).toBe(false);
45
+ expect('headers' in ctx).toBe(false);
46
+ expect('signal' in ctx).toBe(false);
47
+ });
48
+
49
+ it('omits signal key when explicitly passed as undefined', () => {
50
+ const ctx = createProxyContext({ signal: undefined });
51
+
52
+ expect('signal' in ctx).toBe(false);
53
+ });
54
+
55
+ it('preserves an AbortSignal reference', () => {
56
+ const controller = new AbortController();
57
+ const ctx = createProxyContext({ signal: controller.signal });
58
+
59
+ expect(ctx.signal).toBe(controller.signal);
60
+ });
61
+
62
+ it('keeps the original headers reference (Readonly is shallow)', () => {
63
+ const headers = { 'x-trace': 'abc' };
64
+ const ctx = createProxyContext({ headers });
65
+
66
+ expect(ctx.headers).toBe(headers);
67
+ });
68
+
69
+ it('regenerates a UUID when requestId is provided as an empty string', () => {
70
+ const ctx = createProxyContext({ requestId: '' });
71
+
72
+ expect(ctx.requestId).not.toBe('');
73
+ expect(ctx.requestId.length).toBeGreaterThan(0);
74
+ });
75
+
76
+ it('generates distinct requestIds across calls', () => {
77
+ const a = createProxyContext();
78
+ const b = createProxyContext();
79
+
80
+ expect(a.requestId).not.toEqual(b.requestId);
81
+ });
82
+
83
+ it('preserves the proxy identity reference and omits the key when absent', () => {
84
+ const proxy = { name: 'payments-proxy', version: '1.2.3' };
85
+
86
+ expect(createProxyContext({ proxy }).proxy).toBe(proxy);
87
+ expect('proxy' in createProxyContext()).toBe(false);
88
+ expect('proxy' in createProxyContext({ proxy: undefined })).toBe(false);
89
+ });
90
+ });
91
+
92
+ describe('outboundDelegationChain()', () => {
93
+ const identity = (sub: string): Identity => ({
94
+ sub,
95
+ type: 'agent',
96
+ roles: [],
97
+ claims: {},
98
+ resolvedAt: '2026-01-01T00:00:00Z',
99
+ source: 'custom',
100
+ });
101
+
102
+ it('appends identity after the existing delegatedFrom chain oldest-first', () => {
103
+ const root = identity('root');
104
+ const mid = identity('mid');
105
+ const caller = identity('caller');
106
+ const ctx = createProxyContext({
107
+ identity: caller,
108
+ delegatedFrom: [root, mid],
109
+ });
110
+
111
+ expect(outboundDelegationChain(ctx)).toEqual([root, mid, caller]);
112
+ });
113
+
114
+ it('returns an empty array when neither identity nor delegatedFrom is set', () => {
115
+ expect(outboundDelegationChain(createProxyContext())).toEqual([]);
116
+ });
117
+
118
+ it('returns only the identity when delegatedFrom is absent', () => {
119
+ const caller = identity('caller');
120
+ const ctx = createProxyContext({ identity: caller });
121
+
122
+ expect(outboundDelegationChain(ctx)).toEqual([caller]);
123
+ });
124
+
125
+ it('returns a fresh array and does not mutate or alias ctx.delegatedFrom', () => {
126
+ const root = identity('root');
127
+ const delegatedFrom = [root];
128
+ const ctx = createProxyContext({
129
+ identity: identity('caller'),
130
+ delegatedFrom,
131
+ });
132
+
133
+ const chain = outboundDelegationChain(ctx);
134
+
135
+ expect(chain).not.toBe(delegatedFrom);
136
+ expect(ctx.delegatedFrom).toBe(delegatedFrom);
137
+ expect(ctx.delegatedFrom).toEqual([root]);
138
+ expect(outboundDelegationChain(ctx)).not.toBe(chain);
139
+ });
140
+ });