@noodleseed/one 0.138.0 → 0.139.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.
Files changed (70) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/import.d.ts +4 -0
  4. package/dist/commands/import.d.ts.map +1 -0
  5. package/dist/commands/import.js +198 -0
  6. package/dist/commands/import.js.map +1 -0
  7. package/dist/commands/project-setup.d.ts +1 -1
  8. package/dist/commands/project-setup.d.ts.map +1 -1
  9. package/dist/commands/project-setup.js +2 -48
  10. package/dist/commands/project-setup.js.map +1 -1
  11. package/dist/commands/service-ops.d.ts.map +1 -1
  12. package/dist/commands/service-ops.js +44 -51
  13. package/dist/commands/service-ops.js.map +1 -1
  14. package/dist/mcp-import.d.ts +22 -0
  15. package/dist/mcp-import.d.ts.map +1 -0
  16. package/dist/mcp-import.js +91 -0
  17. package/dist/mcp-import.js.map +1 -0
  18. package/dist/self-host-init.d.ts +23 -0
  19. package/dist/self-host-init.d.ts.map +1 -0
  20. package/dist/self-host-init.js +315 -0
  21. package/dist/self-host-init.js.map +1 -0
  22. package/node_modules/@noodle-borg/agent-kit/dist/generated/example-files.js +3 -3
  23. package/node_modules/@noodle-borg/agent-kit/dist/skill-authoring-refs.js +11 -2
  24. package/node_modules/@noodle-borg/agent-kit/dist/skill-connect-refs.js +31 -1
  25. package/node_modules/@noodle-borg/agent-kit/dist/skill-content.js +3 -2
  26. package/node_modules/@noodle-borg/agent-kit/package.json +1 -1
  27. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-guide.d.ts +1 -0
  28. package/node_modules/@noodle-borg/assistant-gateway/dist/assistant-guide.js +35 -1
  29. package/node_modules/@noodle-borg/assistant-gateway/package.json +1 -1
  30. package/node_modules/@noodle-borg/authoring/dist/connections.js +1 -0
  31. package/node_modules/@noodle-borg/authoring/dist/connectors.d.ts +25 -3
  32. package/node_modules/@noodle-borg/authoring/dist/connectors.js +82 -7
  33. package/node_modules/@noodle-borg/authoring/dist/index.d.ts +1 -1
  34. package/node_modules/@noodle-borg/authoring/dist/model-tool-visibility.js +33 -0
  35. package/node_modules/@noodle-borg/authoring/dist/server.d.ts +5 -0
  36. package/node_modules/@noodle-borg/authoring/dist/server.js +7 -2
  37. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-account.js +13 -1
  38. package/node_modules/@noodle-borg/cli-catalog/dist/catalog-data-bootstrap-authoring.js +30 -1
  39. package/node_modules/@noodle-borg/connector-defs/dist/call-graph.js +1 -1
  40. package/node_modules/@noodle-borg/connector-defs/dist/compile-mcp.js +95 -0
  41. package/node_modules/@noodle-borg/connector-defs/dist/compile.js +4 -0
  42. package/node_modules/@noodle-borg/connector-defs/dist/customer-routing.js +1 -1
  43. package/node_modules/@noodle-borg/connector-defs/dist/index.d.ts +1 -1
  44. package/node_modules/@noodle-borg/connector-defs/dist/schema.d.ts +642 -3
  45. package/node_modules/@noodle-borg/connector-defs/dist/schema.js +143 -1
  46. package/node_modules/@noodle-borg/connector-http/dist/index.d.ts +1 -0
  47. package/node_modules/@noodle-borg/connector-http/dist/index.js +1 -0
  48. package/node_modules/@noodle-borg/connector-http/dist/mcp/config.js +76 -0
  49. package/node_modules/@noodle-borg/connector-http/dist/mcp/egress.js +125 -0
  50. package/node_modules/@noodle-borg/connector-http/dist/mcp/failure.js +48 -0
  51. package/node_modules/@noodle-borg/connector-http/dist/mcp/index.d.ts +3 -0
  52. package/node_modules/@noodle-borg/connector-http/dist/mcp/index.js +2 -0
  53. package/node_modules/@noodle-borg/connector-http/dist/mcp/mcp-connector.d.ts +12 -0
  54. package/node_modules/@noodle-borg/connector-http/dist/mcp/mcp-connector.js +125 -0
  55. package/node_modules/@noodle-borg/connector-http/dist/mcp/result.js +55 -0
  56. package/node_modules/@noodle-borg/connector-http/dist/mcp/types.d.ts +52 -0
  57. package/node_modules/@noodle-borg/connector-http/dist/mcp/types.js +10 -0
  58. package/node_modules/@noodle-borg/connector-http/package.json +4 -1
  59. package/node_modules/@noodle-borg/openapi-import/dist/index.js +1 -0
  60. package/node_modules/@noodle-borg/openapi-import/dist/mcp/index.js +4 -0
  61. package/node_modules/@noodle-borg/openapi-import/dist/mcp/probe.js +165 -0
  62. package/node_modules/@noodle-borg/openapi-import/dist/mcp/render.js +99 -0
  63. package/node_modules/@noodle-borg/openapi-import/dist/mcp/snapshot.js +290 -0
  64. package/node_modules/@noodle-borg/openapi-import/package.json +6 -2
  65. package/node_modules/@noodle-borg/runtime/dist/connector/types.d.ts +2 -0
  66. package/node_modules/@noodle-borg/runtime/dist/operation-execution.js +1 -0
  67. package/node_modules/@noodle-borg/service/dist/routes/assistant-agent.js +8 -5
  68. package/node_modules/@noodle-borg/service/package.json +1 -1
  69. package/node_modules/@noodleseed/assistant/package.json +1 -1
  70. package/package.json +2 -2
@@ -276,6 +276,16 @@ const httpAuthSchema = z
276
276
  });
277
277
  }
278
278
  });
279
+ /** Phase-1 upstream MCP auth: broker-owned service credentials only (ADR 0189 Decision 7). */
280
+ const mcpAuthSchema = httpAuthSchema.superRefine((auth, ctx) => {
281
+ if (auth.kind === 'bearer' || auth.kind === 'apiKey' || auth.kind === 'clientCredentials') {
282
+ return;
283
+ }
284
+ ctx.addIssue({
285
+ code: z.ZodIssueCode.custom,
286
+ message: 'MCP connectors currently accept only bearer, apiKey, or clientCredentials auth; delegated upstream OAuth requires the consent-broker slice',
287
+ });
288
+ });
279
289
  /** Public HTTP response allowance: explicit per operation, bounded to cap concurrent memory cost. */
280
290
  const httpOperationLimitsSchema = z
281
291
  .object({
@@ -341,6 +351,54 @@ const httpOperationSchema = z
341
351
  });
342
352
  }
343
353
  });
354
+ const mcpOperationFakeSchema = z.union([
355
+ z.object({ structuredContent: z.record(z.string(), z.unknown()) }).strict(),
356
+ z.object({ text: z.string() }).strict(),
357
+ ]);
358
+ const mcpToolNameSchema = z
359
+ .string()
360
+ .min(1)
361
+ .max(256)
362
+ .refine((value) => !/\p{Cc}/u.test(value), 'upstream MCP tool name contains a control character');
363
+ /** One frozen upstream MCP tool projected as one ordinary connector operation. */
364
+ const mcpOperationSchema = z
365
+ .object({
366
+ type: z.enum(['read', 'action']),
367
+ tool: mcpToolNameSchema,
368
+ input: ioSchema.optional(),
369
+ output: ioSchema.optional(),
370
+ /** `text` means the imported upstream tool had no outputSchema. */
371
+ result: z.enum(['structured', 'text']).default('structured'),
372
+ auth: mcpAuthSchema.optional(),
373
+ limits: httpOperationLimitsSchema.optional(),
374
+ fake: mcpOperationFakeSchema.optional(),
375
+ credentials: operationCredentialsSchema.optional(),
376
+ })
377
+ .strict()
378
+ .superRefine((operation, ctx) => {
379
+ for (const [field, schema] of [
380
+ ['input', operation.input],
381
+ ['output', operation.output],
382
+ ]) {
383
+ if (schema !== undefined && hasExternalSchemaRef(schema)) {
384
+ ctx.addIssue({
385
+ code: z.ZodIssueCode.custom,
386
+ path: [field],
387
+ message: 'external JSON Schema references are not allowed for MCP operations',
388
+ });
389
+ }
390
+ }
391
+ });
392
+ function hasExternalSchemaRef(value) {
393
+ if (Array.isArray(value))
394
+ return value.some(hasExternalSchemaRef);
395
+ if (typeof value !== 'object' || value === null)
396
+ return false;
397
+ const record = value;
398
+ if (typeof record.$ref === 'string' && !record.$ref.startsWith('#'))
399
+ return true;
400
+ return Object.values(record).some(hasExternalSchemaRef);
401
+ }
344
402
  /** Per-operation resource bounds for a compute operation; omitted fields fall back to `DEFAULT_LIMITS`. */
345
403
  const computeLimitsSchema = z
346
404
  .object({
@@ -369,6 +427,61 @@ const computeOperationSchema = z
369
427
  const managedVariableExpressionSchema = z
370
428
  .string()
371
429
  .regex(/^\$\{env\.[A-Za-z0-9_]+\}$/, 'must be an exact managed variable expression');
430
+ const mcpEndpointSchema = z
431
+ .string()
432
+ .min(1)
433
+ .superRefine((value, ctx) => {
434
+ if (managedVariableExpressionSchema.safeParse(value).success)
435
+ return;
436
+ let url;
437
+ try {
438
+ url = new URL(value);
439
+ }
440
+ catch {
441
+ ctx.addIssue({
442
+ code: z.ZodIssueCode.custom,
443
+ message: 'MCP endpoint must be an absolute URL',
444
+ });
445
+ return;
446
+ }
447
+ const loopback = url.protocol === 'http:' &&
448
+ (url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]');
449
+ if (url.protocol !== 'https:' && !loopback) {
450
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'MCP endpoint must use HTTPS' });
451
+ }
452
+ if (url.username !== '' || url.password !== '' || url.hash !== '') {
453
+ ctx.addIssue({
454
+ code: z.ZodIssueCode.custom,
455
+ message: 'MCP endpoint cannot contain userinfo or a fragment',
456
+ });
457
+ }
458
+ });
459
+ const mcpOriginSchema = z
460
+ .string()
461
+ .min(1)
462
+ .superRefine((value, ctx) => {
463
+ if (managedVariableExpressionSchema.safeParse(value).success)
464
+ return;
465
+ let url;
466
+ try {
467
+ url = new URL(value);
468
+ }
469
+ catch {
470
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'MCP origin must be an absolute URL' });
471
+ return;
472
+ }
473
+ const loopback = url.protocol === 'http:' &&
474
+ (url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]');
475
+ if (url.protocol !== 'https:' && !loopback) {
476
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'MCP origin must use HTTPS' });
477
+ }
478
+ if (value !== url.origin) {
479
+ ctx.addIssue({
480
+ code: z.ZodIssueCode.custom,
481
+ message: 'MCP origin must be a canonical bare origin',
482
+ });
483
+ }
484
+ });
372
485
  const connectorIdentity = {
373
486
  id: z.string().min(1),
374
487
  version: z.string().min(1),
@@ -394,6 +507,31 @@ const httpConnectorDefSchema = z
394
507
  operations: z.record(z.string(), httpOperationSchema),
395
508
  })
396
509
  .strict();
510
+ /** A governed remote MCP connector with a compile-time-frozen tool snapshot. */
511
+ const mcpConnectorDefSchema = z
512
+ .object({
513
+ ...connectorIdentity,
514
+ mcp: z
515
+ .object({
516
+ endpoint: mcpEndpointSchema,
517
+ allowedOrigins: z.array(mcpOriginSchema).min(1).max(32).optional(),
518
+ auth: mcpAuthSchema.optional(),
519
+ timeoutMs: z.number().int().positive().max(300_000).optional(),
520
+ maxResponseBytes: z
521
+ .number()
522
+ .int()
523
+ .positive()
524
+ .max(6 * 1024 * 1024)
525
+ .optional(),
526
+ protocol: z.enum(['auto', 'legacy', 'modern']).optional(),
527
+ })
528
+ .strict(),
529
+ operations: z
530
+ .record(z.string(), mcpOperationSchema)
531
+ .refine((operations) => Object.keys(operations).length > 0, 'MCP connector needs an operation')
532
+ .refine((operations) => Object.keys(operations).length <= 256, 'MCP connector supports at most 256 curated operations'),
533
+ })
534
+ .strict();
397
535
  /** A compute connector: sandboxed-code operations, no `http` block. */
398
536
  const computeConnectorDefSchema = z
399
537
  .object({
@@ -405,7 +543,11 @@ const computeConnectorDefSchema = z
405
543
  * A connector is **either** an HTTP connector (has an `http` block) **or** a compute connector (no `http`;
406
544
  * operations carry `code`). Both shapes are `.strict()`, so a given entry matches exactly one branch.
407
545
  */
408
- const connectorDefSchema = z.union([httpConnectorDefSchema, computeConnectorDefSchema]);
546
+ const connectorDefSchema = z.union([
547
+ httpConnectorDefSchema,
548
+ mcpConnectorDefSchema,
549
+ computeConnectorDefSchema,
550
+ ]);
409
551
  export const connectorFileSchema = z
410
552
  .object({
411
553
  connectors: z.array(connectorDefSchema).min(1),
@@ -1,4 +1,5 @@
1
1
  export { type HttpAuthScheme, HttpConnector, type HttpConnectorConfig, type HttpOperation, type HttpOperationFake, type HttpOperationPagination, type HttpPaginationAggregate, type HttpPaginationStopReason, } from './http-connector.js';
2
+ export * from './mcp/index.js';
2
3
  export type { HttpOperationProjection } from './projection.js';
3
4
  export type { GuardedFetchOptions } from './ssrf.js';
4
5
  export { createGuardedAgent, type DnsLookup, guardedFetch, isPublicUnicast, needsGuard, pinnedLookup, } from './ssrf.js';
@@ -1,3 +1,4 @@
1
1
  export { HttpConnector, } from './http-connector.js';
2
+ export * from './mcp/index.js';
2
3
  export { createGuardedAgent, guardedFetch, isPublicUnicast, needsGuard, pinnedLookup, } from './ssrf.js';
3
4
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,76 @@
1
+ import { ConnectorInvocationError } from '@noodle-borg/runtime';
2
+ const VARIABLE = /^\$\{env\.([A-Za-z0-9_]+)\}$/;
3
+ const MAX_RESPONSE_BYTES = 6 * 1024 * 1024;
4
+ export const DEFAULT_RESPONSE_BYTES = 1 << 20;
5
+ export function isVariable(value) {
6
+ return VARIABLE.test(value);
7
+ }
8
+ export function resolveManagedString(configured, env) {
9
+ const match = VARIABLE.exec(configured);
10
+ if (match === null)
11
+ return configured;
12
+ const name = match[1];
13
+ const value = env[name];
14
+ if (value === undefined || value.length === 0) {
15
+ throw new ConnectorInvocationError(`managed variable "${name}" is unavailable`, {
16
+ category: 'invalid_response',
17
+ retryable: false,
18
+ });
19
+ }
20
+ return value;
21
+ }
22
+ export function responseLimit(value) {
23
+ const limit = value ?? DEFAULT_RESPONSE_BYTES;
24
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_RESPONSE_BYTES) {
25
+ throw new ConnectorInvocationError('invalid MCP response size limit', {
26
+ category: 'invalid_response',
27
+ retryable: false,
28
+ });
29
+ }
30
+ return limit;
31
+ }
32
+ export function parsedEndpoint(value) {
33
+ let url;
34
+ try {
35
+ url = new URL(value);
36
+ }
37
+ catch {
38
+ throw new ConnectorInvocationError('upstream MCP endpoint is not a valid URL', {
39
+ category: 'invalid_response',
40
+ retryable: false,
41
+ });
42
+ }
43
+ if (url.username !== '' || url.password !== '' || url.hash !== '') {
44
+ throw new ConnectorInvocationError('upstream MCP endpoint contains forbidden URL components', {
45
+ category: 'invalid_response',
46
+ retryable: false,
47
+ });
48
+ }
49
+ if (!isSecureOrLoopback(url)) {
50
+ throw new ConnectorInvocationError('upstream MCP endpoint must use HTTPS', {
51
+ category: 'invalid_response',
52
+ retryable: false,
53
+ });
54
+ }
55
+ return url;
56
+ }
57
+ export function configuredOrigins(configured, env) {
58
+ const origins = new Set();
59
+ for (const entry of configured) {
60
+ const resolved = resolveManagedString(entry, env);
61
+ const url = parsedEndpoint(resolved);
62
+ if (isVariable(entry) && url.origin !== resolved) {
63
+ throw new ConnectorInvocationError('managed MCP egress origin must resolve to a canonical bare origin', { category: 'invalid_response', retryable: false });
64
+ }
65
+ origins.add(url.origin);
66
+ }
67
+ return origins;
68
+ }
69
+ function isSecureOrLoopback(url) {
70
+ if (url.protocol === 'https:')
71
+ return true;
72
+ if (url.protocol !== 'http:')
73
+ return false;
74
+ return url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]';
75
+ }
76
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,125 @@
1
+ import { ConnectorInvocationError, } from '@noodle-borg/runtime';
2
+ import { guardedFetch } from '../ssrf.js';
3
+ /** Build one invocation-scoped fetch so credential material can never outlive or cross a call. */
4
+ export function invocationFetch(options) {
5
+ return async (input, init = {}) => {
6
+ const url = new URL(input);
7
+ if (!options.allowedOrigins.has(url.origin)) {
8
+ throw new ConnectorInvocationError(`upstream MCP request resolved to a disallowed origin "${url.origin}"`, { category: 'invalid_response', retryable: false });
9
+ }
10
+ const headers = new Headers(init.headers);
11
+ attachCredential(headers, options.call.credentialPresentation ?? options.auth, options.call.credential);
12
+ const request = { ...init, headers, redirect: 'manual' };
13
+ let response;
14
+ try {
15
+ response =
16
+ options.dependencies.fetch === undefined
17
+ ? await guardedFetch(url, request, {
18
+ ...(options.dependencies.lookup === undefined
19
+ ? {}
20
+ : { lookup: options.dependencies.lookup }),
21
+ })
22
+ : await options.dependencies.fetch(url, request);
23
+ }
24
+ catch (error) {
25
+ options.onNetworkFailure();
26
+ throw error;
27
+ }
28
+ if (response.status >= 300 && response.status <= 399) {
29
+ throw new ConnectorInvocationError('upstream MCP redirect rejected', {
30
+ status: response.status,
31
+ category: 'invalid_response',
32
+ retryable: false,
33
+ });
34
+ }
35
+ if (response.status === 429) {
36
+ const retryAfterMs = parseRetryAfterMs(response.headers.get('retry-after'));
37
+ if (retryAfterMs !== undefined)
38
+ options.onRetryAfter(retryAfterMs);
39
+ }
40
+ return boundedResponse(response, options.maxResponseBytes, options.onResponseLimitExceeded);
41
+ };
42
+ }
43
+ function parseRetryAfterMs(value) {
44
+ if (value === null || value.trim() === '')
45
+ return undefined;
46
+ const seconds = Number(value);
47
+ if (Number.isFinite(seconds) && seconds >= 0)
48
+ return Math.round(seconds * 1_000);
49
+ const time = Date.parse(value);
50
+ if (!Number.isFinite(time))
51
+ return undefined;
52
+ return Math.max(0, time - Date.now());
53
+ }
54
+ function attachCredential(headers, scheme, credential) {
55
+ if (scheme === undefined)
56
+ return;
57
+ if (scheme.kind === 'cookie') {
58
+ if (credential.kind !== 'cookie') {
59
+ throw new ConnectorInvocationError('MCP cookie auth requires a cookie credential', {
60
+ category: 'invalid_response',
61
+ retryable: false,
62
+ });
63
+ }
64
+ headers.set('cookie', credential.cookie);
65
+ return;
66
+ }
67
+ if (credential.kind === 'cookie') {
68
+ throw new ConnectorInvocationError('MCP token auth requires a token credential', {
69
+ category: 'invalid_response',
70
+ retryable: false,
71
+ });
72
+ }
73
+ if (scheme.kind === 'bearer')
74
+ headers.set('authorization', `Bearer ${credential.token}`);
75
+ else
76
+ headers.set(scheme.header, credential.token);
77
+ }
78
+ function boundedResponse(response, maxBytes, onExceeded) {
79
+ const length = response.headers.get('content-length');
80
+ if (length !== null && Number(length) > maxBytes) {
81
+ onExceeded();
82
+ throw responseTooLarge();
83
+ }
84
+ if (response.body === null)
85
+ return response;
86
+ const reader = response.body.getReader();
87
+ let size = 0;
88
+ const body = new ReadableStream({
89
+ async pull(controller) {
90
+ try {
91
+ const chunk = await reader.read();
92
+ if (chunk.done) {
93
+ controller.close();
94
+ return;
95
+ }
96
+ size += chunk.value.byteLength;
97
+ if (size > maxBytes) {
98
+ onExceeded();
99
+ await reader.cancel().catch(() => undefined);
100
+ controller.error(responseTooLarge());
101
+ return;
102
+ }
103
+ controller.enqueue(chunk.value);
104
+ }
105
+ catch (error) {
106
+ controller.error(error);
107
+ }
108
+ },
109
+ async cancel(reason) {
110
+ await reader.cancel(reason).catch(() => undefined);
111
+ },
112
+ });
113
+ return new Response(body, {
114
+ status: response.status,
115
+ statusText: response.statusText,
116
+ headers: response.headers,
117
+ });
118
+ }
119
+ function responseTooLarge() {
120
+ return new ConnectorInvocationError('upstream MCP response exceeds the size limit', {
121
+ category: 'response_too_large',
122
+ retryable: false,
123
+ });
124
+ }
125
+ //# sourceMappingURL=egress.js.map
@@ -0,0 +1,48 @@
1
+ import { ProtocolError, SdkError, SdkErrorCode, SdkHttpError } from '@modelcontextprotocol/client';
2
+ import { ConnectorInvocationError, isConnectorInvocationError } from '@noodle-borg/runtime';
3
+ export function normalizeMcpFailure(error, options) {
4
+ if (isConnectorInvocationError(error))
5
+ return error;
6
+ if (options.timedOut ||
7
+ (SdkError.isInstance(error) && error.code === SdkErrorCode.RequestTimeout)) {
8
+ return new ConnectorInvocationError('upstream MCP request timed out', {
9
+ category: 'timeout',
10
+ retryable: false,
11
+ });
12
+ }
13
+ if (SdkHttpError.isInstance(error)) {
14
+ return new ConnectorInvocationError('upstream MCP transport rejected the request', {
15
+ status: error.status,
16
+ category: categoryForStatus(error.status),
17
+ retryable: false,
18
+ ...(options.retryAfterMs === undefined ? {} : { retryAfterMs: options.retryAfterMs }),
19
+ });
20
+ }
21
+ if (SdkError.isInstance(error) &&
22
+ (error.code === SdkErrorCode.UnsupportedResultType ||
23
+ error.code === SdkErrorCode.InvalidResult ||
24
+ error.code === SdkErrorCode.EraNegotiationFailed)) {
25
+ return new ConnectorInvocationError('upstream MCP server returned an unsupported response', {
26
+ category: 'invalid_response',
27
+ retryable: false,
28
+ });
29
+ }
30
+ if (ProtocolError.isInstance(error)) {
31
+ return new ConnectorInvocationError('upstream MCP protocol request failed', {
32
+ category: error.code === -32601 ? 'upstream_4xx' : 'invalid_response',
33
+ retryable: false,
34
+ });
35
+ }
36
+ return new ConnectorInvocationError('upstream MCP network request failed', {
37
+ category: 'network_error',
38
+ retryable: false,
39
+ });
40
+ }
41
+ function categoryForStatus(status) {
42
+ if (status === 429)
43
+ return 'rate_limited';
44
+ if (status >= 500)
45
+ return 'upstream_5xx';
46
+ return 'upstream_4xx';
47
+ }
48
+ //# sourceMappingURL=failure.js.map
@@ -0,0 +1,3 @@
1
+ export { McpConnector } from './mcp-connector.js';
2
+ export type { McpAuthScheme, McpConnectorConfig, McpConnectorDependencies, McpOperation, McpOperationFake, } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ export { McpConnector } from './mcp-connector.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,12 @@
1
+ import type { Connector, ConnectorCall } from '@noodle-borg/runtime';
2
+ import { type McpConnectorConfig, type McpConnectorDependencies } from './types.js';
3
+ /** Governed, invocation-scoped client for one imported remote Streamable HTTP MCP server. */
4
+ export declare class McpConnector implements Connector {
5
+ #private;
6
+ readonly id: string;
7
+ readonly version: string;
8
+ constructor(config: McpConnectorConfig, dependencies?: McpConnectorDependencies);
9
+ signature(operation: string): import("@noodle-borg/compiler").OperationSignature | undefined;
10
+ invoke(call: ConnectorCall): Promise<unknown>;
11
+ }
12
+ //# sourceMappingURL=mcp-connector.d.ts.map
@@ -0,0 +1,125 @@
1
+ import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
2
+ import { ConnectorInvocationError } from '@noodle-borg/runtime';
3
+ import { configuredOrigins, isVariable, parsedEndpoint, resolveManagedString, responseLimit, } from './config.js';
4
+ import { invocationFetch } from './egress.js';
5
+ import { normalizeMcpFailure } from './failure.js';
6
+ import { normalizeToolResult } from './result.js';
7
+ import { toolDefinition } from './types.js';
8
+ const DEFAULT_TIMEOUT_MS = 10_000;
9
+ /** Governed, invocation-scoped client for one imported remote Streamable HTTP MCP server. */
10
+ export class McpConnector {
11
+ id;
12
+ version;
13
+ #config;
14
+ #dependencies;
15
+ constructor(config, dependencies = {}) {
16
+ this.id = config.id;
17
+ this.version = config.version;
18
+ this.#config = config;
19
+ this.#dependencies = dependencies;
20
+ if (Object.keys(config.operations).length === 0) {
21
+ throw new Error('an MCP connector must declare at least one curated operation');
22
+ }
23
+ if (isVariable(config.endpoint) && config.allowedOrigins === undefined) {
24
+ throw new Error('mcp.allowedOrigins is required when endpoint is a managed variable');
25
+ }
26
+ if (!isVariable(config.endpoint))
27
+ parsedEndpoint(config.endpoint);
28
+ responseLimit(config.maxResponseBytes);
29
+ }
30
+ signature(operation) {
31
+ return this.#config.operations[operation]?.signature;
32
+ }
33
+ async invoke(call) {
34
+ const operation = this.#config.operations[call.operation];
35
+ if (operation === undefined) {
36
+ throw new Error(`connector "${this.id}" has no operation "${call.operation}"`);
37
+ }
38
+ const maxBytes = responseLimit(operation.maxResponseBytes ?? this.#config.maxResponseBytes);
39
+ if (this.#config.fakeMode === true) {
40
+ if (operation.fake === undefined) {
41
+ throw new ConnectorInvocationError(`fake MCP response missing for operation "${call.operation}"`, { category: 'invalid_response', retryable: false });
42
+ }
43
+ return normalizeToolResult('structuredContent' in operation.fake
44
+ ? { structuredContent: operation.fake.structuredContent, content: [] }
45
+ : { content: [{ type: 'text', text: operation.fake.text }] }, maxBytes);
46
+ }
47
+ const env = call.env ?? {};
48
+ const endpoint = parsedEndpoint(resolveManagedString(this.#config.endpoint, env));
49
+ const allowed = configuredOrigins(this.#config.allowedOrigins ?? [this.#config.endpoint], env);
50
+ if (!allowed.has(endpoint.origin)) {
51
+ throw new ConnectorInvocationError(`upstream MCP endpoint resolved to a disallowed origin "${endpoint.origin}"`, { category: 'invalid_response', retryable: false });
52
+ }
53
+ const timeoutMs = this.#config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
54
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
55
+ const signal = call.signal === undefined ? timeoutSignal : AbortSignal.any([timeoutSignal, call.signal]);
56
+ const client = new Client({ name: 'noodle-mcp-connector', version: '1.0.0' }, {
57
+ capabilities: {},
58
+ versionNegotiation: { mode: protocolMode(this.#config.protocol) },
59
+ inputRequired: { autoFulfill: false },
60
+ });
61
+ let responseLimitExceeded = false;
62
+ let networkFailure = false;
63
+ let retryAfterMs;
64
+ const transport = new StreamableHTTPClientTransport(endpoint, {
65
+ fetch: invocationFetch({
66
+ allowedOrigins: allowed,
67
+ ...((operation.auth ?? this.#config.auth) === undefined
68
+ ? {}
69
+ : { auth: operation.auth ?? this.#config.auth }),
70
+ call,
71
+ maxResponseBytes: maxBytes,
72
+ dependencies: this.#dependencies,
73
+ onResponseLimitExceeded: () => {
74
+ responseLimitExceeded = true;
75
+ },
76
+ onNetworkFailure: () => {
77
+ networkFailure = true;
78
+ },
79
+ onRetryAfter: (value) => {
80
+ retryAfterMs = value;
81
+ },
82
+ }),
83
+ onInsufficientScope: 'throw',
84
+ });
85
+ const closeTransportOnAbort = () => {
86
+ void transport.close().catch(() => undefined);
87
+ };
88
+ signal.addEventListener('abort', closeTransportOnAbort, { once: true });
89
+ if (signal.aborted)
90
+ closeTransportOnAbort();
91
+ try {
92
+ await client.connect(transport, { signal, timeout: timeoutMs });
93
+ const result = await client.callTool({ name: operation.upstreamTool, arguments: { ...call.args } }, { signal, timeout: timeoutMs, toolDefinition: toolDefinition(operation) });
94
+ return normalizeToolResult(result, maxBytes);
95
+ }
96
+ catch (error) {
97
+ if (responseLimitExceeded) {
98
+ throw new ConnectorInvocationError('upstream MCP response exceeds the size limit', {
99
+ category: 'response_too_large',
100
+ retryable: false,
101
+ });
102
+ }
103
+ if (networkFailure && !signal.aborted) {
104
+ throw new ConnectorInvocationError('upstream MCP network request failed', {
105
+ category: 'network_error',
106
+ retryable: false,
107
+ });
108
+ }
109
+ throw normalizeMcpFailure(error, {
110
+ timedOut: signal.aborted,
111
+ ...(retryAfterMs === undefined ? {} : { retryAfterMs }),
112
+ });
113
+ }
114
+ finally {
115
+ signal.removeEventListener('abort', closeTransportOnAbort);
116
+ await client.close().catch(() => undefined);
117
+ }
118
+ }
119
+ }
120
+ function protocolMode(protocol) {
121
+ if (protocol === 'modern')
122
+ return { pin: '2026-07-28' };
123
+ return protocol ?? 'auto';
124
+ }
125
+ //# sourceMappingURL=mcp-connector.js.map
@@ -0,0 +1,55 @@
1
+ import { ConnectorInvocationError } from '@noodle-borg/runtime';
2
+ export function normalizeToolResult(result, maxBytes) {
3
+ if (result.isError === true) {
4
+ throw new ConnectorInvocationError('upstream MCP tool reported an execution error', {
5
+ category: 'upstream_4xx',
6
+ retryable: false,
7
+ });
8
+ }
9
+ if (result.structuredContent !== undefined) {
10
+ if (!isRecord(result.structuredContent)) {
11
+ throw invalidResult('upstream MCP structured content must be an object');
12
+ }
13
+ assertEncodedSize(result.structuredContent, maxBytes);
14
+ return result.structuredContent;
15
+ }
16
+ const text = result.content
17
+ .filter((block) => isTextBlock(block))
18
+ .map((block) => block.text)
19
+ .join('\n');
20
+ if (text.length === 0) {
21
+ throw invalidResult('upstream MCP tool returned no structured or text content');
22
+ }
23
+ assertEncodedSize(text, maxBytes);
24
+ return { text };
25
+ }
26
+ function isTextBlock(value) {
27
+ if (!isRecord(value))
28
+ return false;
29
+ return value.type === 'text' && typeof value.text === 'string';
30
+ }
31
+ function isRecord(value) {
32
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
33
+ }
34
+ function assertEncodedSize(value, maxBytes) {
35
+ let encoded;
36
+ try {
37
+ encoded = typeof value === 'string' ? value : JSON.stringify(value);
38
+ }
39
+ catch {
40
+ throw invalidResult('upstream MCP result was not serializable');
41
+ }
42
+ if (new TextEncoder().encode(encoded).byteLength > maxBytes) {
43
+ throw new ConnectorInvocationError('upstream MCP result exceeds the size limit', {
44
+ category: 'response_too_large',
45
+ retryable: false,
46
+ });
47
+ }
48
+ }
49
+ function invalidResult(message) {
50
+ return new ConnectorInvocationError(message, {
51
+ category: 'invalid_response',
52
+ retryable: false,
53
+ });
54
+ }
55
+ //# sourceMappingURL=result.js.map