@ttoss/http-server-mcp 0.33.2 → 0.34.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/MIGRATIONS.md +71 -0
- package/README.md +199 -177
- package/dist/index.cjs +733 -161
- package/dist/index.d.cts +313 -152
- package/dist/index.d.mts +313 -152
- package/dist/index.mjs +730 -160
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -40,7 +40,7 @@ var getApiHeaders = () => {
|
|
|
40
40
|
/**
|
|
41
41
|
* The `Accept-Language` header of the current MCP request, or `undefined`
|
|
42
42
|
* outside a request or when the client sent none. The default locale source
|
|
43
|
-
* for `
|
|
43
|
+
* for `createToolGate`'s `i18n` option.
|
|
44
44
|
*/
|
|
45
45
|
var getRequestLocale = () => {
|
|
46
46
|
return requestContextStore.getStore()?.acceptLanguage;
|
|
@@ -367,24 +367,33 @@ var registerAppResource = ({
|
|
|
367
367
|
};
|
|
368
368
|
|
|
369
369
|
//#endregion
|
|
370
|
-
//#region src/
|
|
370
|
+
//#region src/tool.ts
|
|
371
|
+
var EMPTY_OBJECT_SCHEMA = {
|
|
372
|
+
type: "object",
|
|
373
|
+
properties: {}
|
|
374
|
+
};
|
|
375
|
+
/** The JSON Schema dialect the MCP SDK converts Standard Schemas to. */
|
|
376
|
+
var JSON_SCHEMA_TARGET = "draft-2020-12";
|
|
377
|
+
var isStandardSchema = schema => {
|
|
378
|
+
return "~standard" in schema;
|
|
379
|
+
};
|
|
371
380
|
/**
|
|
372
|
-
* Converts a
|
|
381
|
+
* Converts a JSON Schema into the Standard Schema `registerTool` accepts.
|
|
373
382
|
*
|
|
374
383
|
* A Standard Schema keeps advertisement and enforcement in separate fields:
|
|
375
384
|
* `~standard.jsonSchema` is what `tools/list` publishes, `~standard.validate`
|
|
376
385
|
* is what `tools/call` runs. Replacing only `validate` therefore keeps the
|
|
377
386
|
* schema fully visible to clients while leaving arguments unchecked.
|
|
378
387
|
*/
|
|
379
|
-
var
|
|
380
|
-
|
|
381
|
-
|
|
388
|
+
var fromJsonObjectSchema = ({
|
|
389
|
+
schema,
|
|
390
|
+
validate
|
|
382
391
|
}) => {
|
|
383
|
-
const
|
|
384
|
-
if (
|
|
392
|
+
const standard = (0, _modelcontextprotocol_server.fromJsonSchema)(schema);
|
|
393
|
+
if (validate) return standard;
|
|
385
394
|
return {
|
|
386
395
|
"~standard": {
|
|
387
|
-
...
|
|
396
|
+
...standard["~standard"],
|
|
388
397
|
validate: value => {
|
|
389
398
|
return {
|
|
390
399
|
value
|
|
@@ -393,71 +402,53 @@ var toStandardSchema = ({
|
|
|
393
402
|
}
|
|
394
403
|
};
|
|
395
404
|
};
|
|
405
|
+
var resolved = /* @__PURE__ */new WeakMap();
|
|
396
406
|
/**
|
|
397
|
-
*
|
|
398
|
-
* `
|
|
399
|
-
*
|
|
400
|
-
* This is useful when tool definitions are shared between the MCP server and
|
|
401
|
-
* an AI SDK agent (e.g. Vercel AI SDK's `tool()` helper), because both consume
|
|
402
|
-
* a plain JSON Schema at runtime. Using this helper eliminates the lossy
|
|
403
|
-
* JSON-Schema→Zod conversion that would otherwise be required.
|
|
404
|
-
*
|
|
405
|
-
* Thin wrapper over `@modelcontextprotocol/server`'s `fromJsonSchema`, which
|
|
406
|
-
* converts a JSON Schema into a Standard Schema that `registerTool` accepts
|
|
407
|
-
* directly, so the schema round-trips verbatim over `tools/list`. Arguments
|
|
408
|
-
* reach the handler unchecked unless `validateArguments` is enabled.
|
|
409
|
-
*
|
|
410
|
-
* @param server - The `McpServer` instance to register the tool on.
|
|
411
|
-
* @param params - Tool configuration including name, description, inputSchema,
|
|
412
|
-
* validateArguments, `_meta`, and handler.
|
|
413
|
-
*
|
|
414
|
-
* @example
|
|
415
|
-
* ```typescript
|
|
416
|
-
* import { registerToolFromSchema, McpServer } from '@ttoss/http-server-mcp';
|
|
417
|
-
*
|
|
418
|
-
* const server = new McpServer({ name: 'my-server', version: '1.0.0' });
|
|
419
|
-
*
|
|
420
|
-
* registerToolFromSchema(server, {
|
|
421
|
-
* name: 'get-project',
|
|
422
|
-
* description: 'Get a project by ID',
|
|
423
|
-
* inputSchema: {
|
|
424
|
-
* type: 'object',
|
|
425
|
-
* properties: { id: { type: 'string', description: 'Project public ID' } },
|
|
426
|
-
* required: ['id'],
|
|
427
|
-
* },
|
|
428
|
-
* handler: async ({ id }) => ({
|
|
429
|
-
* content: [{ type: 'text', text: `Project: ${id}` }],
|
|
430
|
-
* }),
|
|
431
|
-
* });
|
|
432
|
-
* ```
|
|
407
|
+
* Resolves a tool's schemas once, so the direct registration and a deferred
|
|
408
|
+
* `call` enforce exactly the same thing.
|
|
433
409
|
*/
|
|
434
|
-
var
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
validateArguments = false,
|
|
443
|
-
handler,
|
|
444
|
-
_meta
|
|
445
|
-
} = params;
|
|
446
|
-
server.registerTool(name, {
|
|
447
|
-
description,
|
|
448
|
-
inputSchema: toStandardSchema({
|
|
449
|
-
inputSchema,
|
|
450
|
-
validateArguments
|
|
410
|
+
var resolveToolSchemas = tool => {
|
|
411
|
+
const cached = resolved.get(tool);
|
|
412
|
+
if (cached) return cached;
|
|
413
|
+
const inputSchema = tool.inputSchema ?? EMPTY_OBJECT_SCHEMA;
|
|
414
|
+
const result = {
|
|
415
|
+
input: isStandardSchema(inputSchema) ? inputSchema : fromJsonObjectSchema({
|
|
416
|
+
schema: inputSchema,
|
|
417
|
+
validate: tool.validateArguments ?? false
|
|
451
418
|
}),
|
|
452
|
-
|
|
419
|
+
inputJson: isStandardSchema(inputSchema) ? inputSchema["~standard"].jsonSchema.input({
|
|
420
|
+
target: JSON_SCHEMA_TARGET
|
|
421
|
+
}) : inputSchema,
|
|
422
|
+
output: tool.outputSchema === void 0 || isStandardSchema(tool.outputSchema) ? tool.outputSchema : fromJsonObjectSchema({
|
|
423
|
+
schema: tool.outputSchema,
|
|
424
|
+
validate: true
|
|
425
|
+
})
|
|
426
|
+
};
|
|
427
|
+
resolved.set(tool, result);
|
|
428
|
+
return result;
|
|
429
|
+
};
|
|
430
|
+
/** Registers one tool as one MCP tool. */
|
|
431
|
+
var registerDirectTool = ({
|
|
432
|
+
server,
|
|
433
|
+
tool
|
|
434
|
+
}) => {
|
|
435
|
+
const schemas = resolveToolSchemas(tool);
|
|
436
|
+
server.registerTool(tool.name, {
|
|
437
|
+
title: tool.title,
|
|
438
|
+
description: tool.description,
|
|
439
|
+
inputSchema: schemas.input,
|
|
440
|
+
outputSchema: schemas.output,
|
|
441
|
+
annotations: tool.annotations,
|
|
442
|
+
_meta: tool._meta
|
|
453
443
|
}, async args => {
|
|
454
|
-
return handler(args);
|
|
444
|
+
return tool.handler(args);
|
|
455
445
|
});
|
|
456
446
|
};
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
447
|
+
/** An `isError` result whose text is `{ error, code? }` as JSON. */
|
|
448
|
+
var toolError = ({
|
|
449
|
+
message,
|
|
450
|
+
code
|
|
451
|
+
}) => {
|
|
461
452
|
return {
|
|
462
453
|
content: [{
|
|
463
454
|
type: "text",
|
|
@@ -472,6 +463,591 @@ var toolError = (message, code) => {
|
|
|
472
463
|
};
|
|
473
464
|
};
|
|
474
465
|
/**
|
|
466
|
+
* Builds a {@link Tool} from a `method` that returns data instead of a
|
|
467
|
+
* `CallToolResult`, which is what most hand-written tools want.
|
|
468
|
+
*
|
|
469
|
+
* @example
|
|
470
|
+
* ```typescript
|
|
471
|
+
* const getCampaign = defineTool({
|
|
472
|
+
* name: 'get-campaign',
|
|
473
|
+
* description: 'Get a campaign by id.',
|
|
474
|
+
* inputSchema: z.object({ campaignId: z.string() }),
|
|
475
|
+
* method: ({ campaignId }) => findCampaign(campaignId as string),
|
|
476
|
+
* notFoundMessage: 'Campaign not found',
|
|
477
|
+
* });
|
|
478
|
+
* ```
|
|
479
|
+
*/
|
|
480
|
+
var defineTool = ({
|
|
481
|
+
method,
|
|
482
|
+
notFoundMessage = "Not found",
|
|
483
|
+
...tool
|
|
484
|
+
}) => {
|
|
485
|
+
return {
|
|
486
|
+
...tool,
|
|
487
|
+
handler: async args => {
|
|
488
|
+
const result = await method(args);
|
|
489
|
+
if (result == null) return toolError({
|
|
490
|
+
message: notFoundMessage
|
|
491
|
+
});
|
|
492
|
+
const content = [{
|
|
493
|
+
type: "text",
|
|
494
|
+
text: JSON.stringify(result)
|
|
495
|
+
}];
|
|
496
|
+
return tool.outputSchema === void 0 ? {
|
|
497
|
+
content
|
|
498
|
+
} : {
|
|
499
|
+
content,
|
|
500
|
+
structuredContent: result
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region src/toolSearch.ts
|
|
508
|
+
var SUMMARY_MAX_LENGTH = 160;
|
|
509
|
+
var SUGGESTION_COUNT = 3;
|
|
510
|
+
var MIN_RELATED_SCORE = 1;
|
|
511
|
+
/** Plural and singular forms rank alike: `agents` finds `get-agent`. */
|
|
512
|
+
var stem = term => {
|
|
513
|
+
if (term.length > 4 && term.endsWith("ies")) return `${term.slice(0, -3)}y`;
|
|
514
|
+
if (term.length > 3 && term.endsWith("s") && !term.endsWith("ss")) return term.slice(0, -1);
|
|
515
|
+
return term;
|
|
516
|
+
};
|
|
517
|
+
/** Splits kebab-case, snake_case, camelCase and prose into lowercase terms. */
|
|
518
|
+
var termsOf = text => {
|
|
519
|
+
return text.replace(/([\p{Ll}\p{N}])(\p{Lu})/gu, "$1 $2").toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(term => {
|
|
520
|
+
return term.length > 0;
|
|
521
|
+
}).map(stem);
|
|
522
|
+
};
|
|
523
|
+
/** The fields a query term is matched against, most telling first. */
|
|
524
|
+
var FIELDS = [{
|
|
525
|
+
weight: 4,
|
|
526
|
+
text: tool => {
|
|
527
|
+
return tool.name;
|
|
528
|
+
}
|
|
529
|
+
}, {
|
|
530
|
+
weight: 3,
|
|
531
|
+
text: tool => {
|
|
532
|
+
return tool.summary ?? "";
|
|
533
|
+
}
|
|
534
|
+
}, {
|
|
535
|
+
weight: 2,
|
|
536
|
+
text: tool => {
|
|
537
|
+
return (tool.tags ?? []).join(" ");
|
|
538
|
+
}
|
|
539
|
+
}, {
|
|
540
|
+
weight: 1,
|
|
541
|
+
text: tool => {
|
|
542
|
+
return tool.description;
|
|
543
|
+
}
|
|
544
|
+
}];
|
|
545
|
+
var fieldTerms = /* @__PURE__ */new WeakMap();
|
|
546
|
+
var termsByField = tool => {
|
|
547
|
+
const cached = fieldTerms.get(tool);
|
|
548
|
+
if (cached) return cached;
|
|
549
|
+
const terms = FIELDS.map(field => {
|
|
550
|
+
return new Set(termsOf(field.text(tool)));
|
|
551
|
+
});
|
|
552
|
+
fieldTerms.set(tool, terms);
|
|
553
|
+
return terms;
|
|
554
|
+
};
|
|
555
|
+
/**
|
|
556
|
+
* A term scores the weight of the best field holding it, half that when it
|
|
557
|
+
* is only a prefix of a word there (`proj` → `project`).
|
|
558
|
+
*/
|
|
559
|
+
var scoreOf = ({
|
|
560
|
+
tool,
|
|
561
|
+
terms
|
|
562
|
+
}) => {
|
|
563
|
+
const fields = termsByField(tool);
|
|
564
|
+
let score = 0;
|
|
565
|
+
for (const term of terms) {
|
|
566
|
+
let best = 0;
|
|
567
|
+
for (const [index, words] of fields.entries()) {
|
|
568
|
+
const weight = FIELDS[index].weight;
|
|
569
|
+
if (words.has(term)) best = Math.max(best, weight);else if (term.length >= 3 && [...words].some(word => {
|
|
570
|
+
return word.startsWith(term);
|
|
571
|
+
})) best = Math.max(best, weight / 2);
|
|
572
|
+
}
|
|
573
|
+
score += best;
|
|
574
|
+
}
|
|
575
|
+
return score;
|
|
576
|
+
};
|
|
577
|
+
var withTag = ({
|
|
578
|
+
tag,
|
|
579
|
+
tools
|
|
580
|
+
}) => {
|
|
581
|
+
return tag === void 0 ? tools : tools.filter(tool => {
|
|
582
|
+
return tool.tags?.includes(tag) ?? false;
|
|
583
|
+
});
|
|
584
|
+
};
|
|
585
|
+
/**
|
|
586
|
+
* The tools `query` matches, best first, ties in the tools' order; `undefined`
|
|
587
|
+
* when the query has no words.
|
|
588
|
+
*/
|
|
589
|
+
var scoreTools = ({
|
|
590
|
+
query,
|
|
591
|
+
tag,
|
|
592
|
+
tools
|
|
593
|
+
}) => {
|
|
594
|
+
const terms = [...new Set(termsOf(query))];
|
|
595
|
+
if (terms.length === 0) return void 0;
|
|
596
|
+
return withTag({
|
|
597
|
+
tag,
|
|
598
|
+
tools
|
|
599
|
+
}).map((tool, index) => {
|
|
600
|
+
return {
|
|
601
|
+
tool,
|
|
602
|
+
index,
|
|
603
|
+
score: scoreOf({
|
|
604
|
+
tool,
|
|
605
|
+
terms
|
|
606
|
+
})
|
|
607
|
+
};
|
|
608
|
+
}).filter(({
|
|
609
|
+
score
|
|
610
|
+
}) => {
|
|
611
|
+
return score > 0;
|
|
612
|
+
}).sort((a, b) => {
|
|
613
|
+
return b.score - a.score || a.index - b.index;
|
|
614
|
+
});
|
|
615
|
+
};
|
|
616
|
+
/**
|
|
617
|
+
* The deferred tools' default search: scores each tool by the query's terms,
|
|
618
|
+
* weighted name > summary > tags > description, and breaks ties by the
|
|
619
|
+
* tools' order. No index — a few hundred tools need none.
|
|
620
|
+
*/
|
|
621
|
+
var rankTools = ({
|
|
622
|
+
query,
|
|
623
|
+
tag,
|
|
624
|
+
limit,
|
|
625
|
+
tools
|
|
626
|
+
}) => {
|
|
627
|
+
const scored = scoreTools({
|
|
628
|
+
query,
|
|
629
|
+
tag,
|
|
630
|
+
tools
|
|
631
|
+
});
|
|
632
|
+
if (!scored) return withTag({
|
|
633
|
+
tag,
|
|
634
|
+
tools
|
|
635
|
+
}).slice(0, limit);
|
|
636
|
+
return scored.slice(0, limit).map(({
|
|
637
|
+
tool
|
|
638
|
+
}) => {
|
|
639
|
+
return tool;
|
|
640
|
+
});
|
|
641
|
+
};
|
|
642
|
+
var summaryOf = tool => {
|
|
643
|
+
const [line] = (tool.summary ?? tool.description).trim().split("\n", 1);
|
|
644
|
+
return line.length > SUMMARY_MAX_LENGTH ? `${line.slice(0, SUMMARY_MAX_LENGTH - 1).trimEnd()}…` : line;
|
|
645
|
+
};
|
|
646
|
+
var editDistance = (a, b) => {
|
|
647
|
+
const row = Array.from({
|
|
648
|
+
length: b.length + 1
|
|
649
|
+
}, (_, index) => {
|
|
650
|
+
return index;
|
|
651
|
+
});
|
|
652
|
+
for (let i = 1; i <= a.length; i++) {
|
|
653
|
+
let diagonal = row[0];
|
|
654
|
+
row[0] = i;
|
|
655
|
+
for (let j = 1; j <= b.length; j++) {
|
|
656
|
+
const above = row[j];
|
|
657
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
658
|
+
diagonal = above;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return row[b.length];
|
|
662
|
+
};
|
|
663
|
+
/** Names a model probably meant: near-misses first, then related tools. */
|
|
664
|
+
var suggestionsFor = ({
|
|
665
|
+
name,
|
|
666
|
+
tools
|
|
667
|
+
}) => {
|
|
668
|
+
const tolerance = Math.max(2, Math.floor(name.length / 3));
|
|
669
|
+
const nearMisses = tools.map(tool => {
|
|
670
|
+
return {
|
|
671
|
+
name: tool.name,
|
|
672
|
+
distance: editDistance(name, tool.name)
|
|
673
|
+
};
|
|
674
|
+
}).filter(({
|
|
675
|
+
distance
|
|
676
|
+
}) => {
|
|
677
|
+
return distance <= tolerance;
|
|
678
|
+
}).sort((a, b) => {
|
|
679
|
+
return a.distance - b.distance;
|
|
680
|
+
}).map(candidate => {
|
|
681
|
+
return candidate.name;
|
|
682
|
+
});
|
|
683
|
+
const related = (scoreTools({
|
|
684
|
+
query: name,
|
|
685
|
+
tools
|
|
686
|
+
}) ?? []).filter(({
|
|
687
|
+
score
|
|
688
|
+
}) => {
|
|
689
|
+
return score >= MIN_RELATED_SCORE;
|
|
690
|
+
}).slice(0, SUGGESTION_COUNT).map(({
|
|
691
|
+
tool
|
|
692
|
+
}) => {
|
|
693
|
+
return tool.name;
|
|
694
|
+
});
|
|
695
|
+
return [...new Set([...nearMisses, ...related])].slice(0, SUGGESTION_COUNT);
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
//#endregion
|
|
699
|
+
//#region src/deferTools.ts
|
|
700
|
+
var DEFAULT_SEARCH_LIMIT = 10;
|
|
701
|
+
var DEFAULT_SEARCH_MAX_LIMIT = 50;
|
|
702
|
+
var jsonResult = ({
|
|
703
|
+
data,
|
|
704
|
+
isError = false
|
|
705
|
+
}) => {
|
|
706
|
+
return {
|
|
707
|
+
content: [{
|
|
708
|
+
type: "text",
|
|
709
|
+
text: JSON.stringify(data)
|
|
710
|
+
}],
|
|
711
|
+
structuredContent: data,
|
|
712
|
+
...(isError ? {
|
|
713
|
+
isError: true
|
|
714
|
+
} : {})
|
|
715
|
+
};
|
|
716
|
+
};
|
|
717
|
+
var errorMessageOf = error => {
|
|
718
|
+
return error instanceof Error ? error.message : String(error);
|
|
719
|
+
};
|
|
720
|
+
var formatIssue = issue => {
|
|
721
|
+
const path = (issue.path ?? []).map(segment => {
|
|
722
|
+
return String(typeof segment === "object" ? segment.key : segment);
|
|
723
|
+
}).join(".");
|
|
724
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
725
|
+
};
|
|
726
|
+
var linksAppView = ({
|
|
727
|
+
tool
|
|
728
|
+
}) => {
|
|
729
|
+
const ui = tool._meta?.ui;
|
|
730
|
+
return typeof ui === "object" && ui !== null && "resourceUri" in ui || tool._meta?.["ui/resourceUri"] !== void 0;
|
|
731
|
+
};
|
|
732
|
+
var deferredToolsOf = ({
|
|
733
|
+
tools,
|
|
734
|
+
options
|
|
735
|
+
}) => {
|
|
736
|
+
const byName = /* @__PURE__ */new Map();
|
|
737
|
+
for (const tool of tools) {
|
|
738
|
+
if (byName.has(tool.name)) throw new Error(`Two tools are named "${tool.name}".`);
|
|
739
|
+
byName.set(tool.name, tool);
|
|
740
|
+
}
|
|
741
|
+
const isVisible = tool => {
|
|
742
|
+
return options.visible ? options.visible({
|
|
743
|
+
tool
|
|
744
|
+
}) : true;
|
|
745
|
+
};
|
|
746
|
+
const searchLimit = options.searchLimit ?? DEFAULT_SEARCH_LIMIT;
|
|
747
|
+
return {
|
|
748
|
+
names: {
|
|
749
|
+
search: "search",
|
|
750
|
+
describe: "describe",
|
|
751
|
+
call: "call",
|
|
752
|
+
...options.names
|
|
753
|
+
},
|
|
754
|
+
tools,
|
|
755
|
+
searchLimit,
|
|
756
|
+
searchMaxLimit: options.searchMaxLimit ?? Math.max(DEFAULT_SEARCH_MAX_LIMIT, searchLimit),
|
|
757
|
+
searchTools: options.search ?? rankTools,
|
|
758
|
+
visibleTools: () => {
|
|
759
|
+
return tools.filter(isVisible);
|
|
760
|
+
},
|
|
761
|
+
find: name => {
|
|
762
|
+
const tool = byName.get(name);
|
|
763
|
+
return tool && isVisible(tool) ? tool : void 0;
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
};
|
|
767
|
+
var searchToolOf = ({
|
|
768
|
+
names,
|
|
769
|
+
tools,
|
|
770
|
+
searchLimit,
|
|
771
|
+
searchMaxLimit,
|
|
772
|
+
searchTools,
|
|
773
|
+
visibleTools
|
|
774
|
+
}) => {
|
|
775
|
+
const tags = [...new Set(tools.flatMap(tool => {
|
|
776
|
+
return tool.tags ?? [];
|
|
777
|
+
}))].sort();
|
|
778
|
+
return {
|
|
779
|
+
name: names.search,
|
|
780
|
+
description: `Find tools by keyword among the ${tools.length} this server offers. Returns each match's name and summary; get input schemas with "${names.describe}", then run one with "${names.call}".` + (tags.length > 0 ? ` Tags: ${tags.join(", ")}.` : ""),
|
|
781
|
+
inputSchema: {
|
|
782
|
+
type: "object",
|
|
783
|
+
properties: {
|
|
784
|
+
query: {
|
|
785
|
+
type: "string",
|
|
786
|
+
description: "Words describing the action, e.g. \"create agent\". Empty lists the tools in order."
|
|
787
|
+
},
|
|
788
|
+
tag: {
|
|
789
|
+
type: "string",
|
|
790
|
+
description: "Only tools with this tag.",
|
|
791
|
+
...(tags.length > 0 ? {
|
|
792
|
+
enum: tags
|
|
793
|
+
} : {})
|
|
794
|
+
},
|
|
795
|
+
limit: {
|
|
796
|
+
type: "integer",
|
|
797
|
+
minimum: 1,
|
|
798
|
+
maximum: searchMaxLimit,
|
|
799
|
+
description: `Maximum number of results. Defaults to ${searchLimit}.`
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
required: ["query"]
|
|
803
|
+
},
|
|
804
|
+
validateArguments: true,
|
|
805
|
+
annotations: {
|
|
806
|
+
readOnlyHint: true
|
|
807
|
+
},
|
|
808
|
+
handler: async args => {
|
|
809
|
+
const limit = args.limit ?? searchLimit;
|
|
810
|
+
return jsonResult({
|
|
811
|
+
data: {
|
|
812
|
+
tools: (await searchTools({
|
|
813
|
+
query: args.query,
|
|
814
|
+
tag: args.tag,
|
|
815
|
+
limit,
|
|
816
|
+
tools: visibleTools()
|
|
817
|
+
})).slice(0, limit).map(tool => {
|
|
818
|
+
return {
|
|
819
|
+
name: tool.name,
|
|
820
|
+
summary: summaryOf(tool),
|
|
821
|
+
...(tool.tags ? {
|
|
822
|
+
tags: tool.tags
|
|
823
|
+
} : {})
|
|
824
|
+
};
|
|
825
|
+
})
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
};
|
|
831
|
+
var describeToolOf = ({
|
|
832
|
+
names,
|
|
833
|
+
find,
|
|
834
|
+
visibleTools
|
|
835
|
+
}) => {
|
|
836
|
+
return {
|
|
837
|
+
name: names.describe,
|
|
838
|
+
description: `Get the full definition of tools found with "${names.search}", including the input schema "${names.call}" expects.`,
|
|
839
|
+
inputSchema: {
|
|
840
|
+
type: "object",
|
|
841
|
+
properties: {
|
|
842
|
+
names: {
|
|
843
|
+
type: "array",
|
|
844
|
+
items: {
|
|
845
|
+
type: "string"
|
|
846
|
+
},
|
|
847
|
+
minItems: 1,
|
|
848
|
+
description: "Tool names, as search returned them."
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
required: ["names"]
|
|
852
|
+
},
|
|
853
|
+
validateArguments: true,
|
|
854
|
+
annotations: {
|
|
855
|
+
readOnlyHint: true
|
|
856
|
+
},
|
|
857
|
+
handler: args => {
|
|
858
|
+
const described = [];
|
|
859
|
+
const unknown = [];
|
|
860
|
+
for (const name of args.names) {
|
|
861
|
+
const tool = find(name);
|
|
862
|
+
if (!tool) {
|
|
863
|
+
unknown.push({
|
|
864
|
+
name,
|
|
865
|
+
suggestions: suggestionsFor({
|
|
866
|
+
name,
|
|
867
|
+
tools: visibleTools()
|
|
868
|
+
})
|
|
869
|
+
});
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
described.push({
|
|
873
|
+
name: tool.name,
|
|
874
|
+
...(tool.title ? {
|
|
875
|
+
title: tool.title
|
|
876
|
+
} : {}),
|
|
877
|
+
description: tool.description,
|
|
878
|
+
inputSchema: resolveToolSchemas(tool).inputJson,
|
|
879
|
+
...(tool.annotations ? {
|
|
880
|
+
annotations: tool.annotations
|
|
881
|
+
} : {})
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
return jsonResult({
|
|
885
|
+
data: {
|
|
886
|
+
tools: described,
|
|
887
|
+
...(unknown.length > 0 ? {
|
|
888
|
+
unknown
|
|
889
|
+
} : {})
|
|
890
|
+
},
|
|
891
|
+
isError: described.length === 0
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
};
|
|
896
|
+
/** Runs one tool the way a direct call would, with errors that help a retry. */
|
|
897
|
+
var runTool = async ({
|
|
898
|
+
tool,
|
|
899
|
+
args
|
|
900
|
+
}) => {
|
|
901
|
+
const schemas = resolveToolSchemas(tool);
|
|
902
|
+
const validated = await schemas.input["~standard"].validate(args);
|
|
903
|
+
if (validated.issues) return jsonResult({
|
|
904
|
+
data: {
|
|
905
|
+
error: `Invalid arguments for "${tool.name}".`,
|
|
906
|
+
issues: validated.issues.map(formatIssue),
|
|
907
|
+
inputSchema: schemas.inputJson
|
|
908
|
+
},
|
|
909
|
+
isError: true
|
|
910
|
+
});
|
|
911
|
+
const schemaHint = {
|
|
912
|
+
type: "text",
|
|
913
|
+
text: JSON.stringify({
|
|
914
|
+
inputSchema: schemas.inputJson
|
|
915
|
+
})
|
|
916
|
+
};
|
|
917
|
+
try {
|
|
918
|
+
const result = await tool.handler(validated.value);
|
|
919
|
+
return result.isError ? {
|
|
920
|
+
...result,
|
|
921
|
+
content: [...result.content, schemaHint]
|
|
922
|
+
} : result;
|
|
923
|
+
} catch (error) {
|
|
924
|
+
return {
|
|
925
|
+
content: [{
|
|
926
|
+
type: "text",
|
|
927
|
+
text: errorMessageOf(error)
|
|
928
|
+
}, schemaHint],
|
|
929
|
+
isError: true
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
var callToolOf = ({
|
|
934
|
+
names,
|
|
935
|
+
find,
|
|
936
|
+
visibleTools
|
|
937
|
+
}) => {
|
|
938
|
+
return {
|
|
939
|
+
name: names.call,
|
|
940
|
+
description: `Run a tool found with "${names.search}". Pass its arguments as "${names.describe}" defined them.`,
|
|
941
|
+
inputSchema: {
|
|
942
|
+
type: "object",
|
|
943
|
+
properties: {
|
|
944
|
+
name: {
|
|
945
|
+
type: "string",
|
|
946
|
+
description: "The tool to run."
|
|
947
|
+
},
|
|
948
|
+
arguments: {
|
|
949
|
+
type: "object",
|
|
950
|
+
description: "The tool's arguments, matching its input schema."
|
|
951
|
+
}
|
|
952
|
+
},
|
|
953
|
+
required: ["name"]
|
|
954
|
+
},
|
|
955
|
+
validateArguments: true,
|
|
956
|
+
handler: args => {
|
|
957
|
+
const name = args.name;
|
|
958
|
+
const tool = find(name);
|
|
959
|
+
if (!tool) return jsonResult({
|
|
960
|
+
data: {
|
|
961
|
+
error: `Unknown tool "${name}".`,
|
|
962
|
+
suggestions: suggestionsFor({
|
|
963
|
+
name,
|
|
964
|
+
tools: visibleTools()
|
|
965
|
+
})
|
|
966
|
+
},
|
|
967
|
+
isError: true
|
|
968
|
+
});
|
|
969
|
+
return runTool({
|
|
970
|
+
tool,
|
|
971
|
+
args: args.arguments ?? {}
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
};
|
|
976
|
+
/**
|
|
977
|
+
* Defers `tools` behind three tools — `search`, `describe`, `call` — so a
|
|
978
|
+
* client's context holds three small definitions instead of every schema,
|
|
979
|
+
* and a schema enters it only for the tools the model is about to use.
|
|
980
|
+
*/
|
|
981
|
+
var registerDeferredTools = ({
|
|
982
|
+
server,
|
|
983
|
+
tools,
|
|
984
|
+
...options
|
|
985
|
+
}) => {
|
|
986
|
+
const deferred = deferredToolsOf({
|
|
987
|
+
tools,
|
|
988
|
+
options
|
|
989
|
+
});
|
|
990
|
+
for (const tool of [searchToolOf(deferred), describeToolOf(deferred), callToolOf(deferred)]) registerDirectTool({
|
|
991
|
+
server,
|
|
992
|
+
tool
|
|
993
|
+
});
|
|
994
|
+
const except = options.except ?? linksAppView;
|
|
995
|
+
for (const tool of tools) if (except({
|
|
996
|
+
tool
|
|
997
|
+
})) registerDirectTool({
|
|
998
|
+
server,
|
|
999
|
+
tool
|
|
1000
|
+
});
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
//#endregion
|
|
1004
|
+
//#region src/registerTools.ts
|
|
1005
|
+
/**
|
|
1006
|
+
* Registers tools on an MCP server, either one MCP tool each (the default)
|
|
1007
|
+
* or deferred behind `search` / `describe` / `call`. The same `tools` can feed
|
|
1008
|
+
* both, so two endpoints over one API cannot drift apart.
|
|
1009
|
+
*
|
|
1010
|
+
* @example
|
|
1011
|
+
* ```typescript
|
|
1012
|
+
* import { McpServer, registerTools, z } from '@ttoss/http-server-mcp';
|
|
1013
|
+
*
|
|
1014
|
+
* const tools = [
|
|
1015
|
+
* {
|
|
1016
|
+
* name: 'get-project',
|
|
1017
|
+
* description: 'Get a project by ID',
|
|
1018
|
+
* inputSchema: z.object({ id: z.string() }),
|
|
1019
|
+
* handler: async ({ id }) => ({
|
|
1020
|
+
* content: [{ type: 'text', text: `Project: ${id}` }],
|
|
1021
|
+
* }),
|
|
1022
|
+
* },
|
|
1023
|
+
* ];
|
|
1024
|
+
*
|
|
1025
|
+
* registerTools({ server: fullServer, tools });
|
|
1026
|
+
* registerTools({ server: deferredServer, tools, defer: true });
|
|
1027
|
+
* ```
|
|
1028
|
+
*/
|
|
1029
|
+
var registerTools = ({
|
|
1030
|
+
server,
|
|
1031
|
+
tools,
|
|
1032
|
+
defer = false
|
|
1033
|
+
}) => {
|
|
1034
|
+
if (defer === false) {
|
|
1035
|
+
for (const tool of tools) registerDirectTool({
|
|
1036
|
+
server,
|
|
1037
|
+
tool
|
|
1038
|
+
});
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
registerDeferredTools({
|
|
1042
|
+
server,
|
|
1043
|
+
tools,
|
|
1044
|
+
...(defer === true ? {} : defer)
|
|
1045
|
+
});
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
//#endregion
|
|
1049
|
+
//#region src/toolGate.ts
|
|
1050
|
+
/**
|
|
475
1051
|
* The localized `isError` result for `error`, or `undefined` when it is not
|
|
476
1052
|
* a `LocalizedError` or rendering fails — the caller then keeps its usual path.
|
|
477
1053
|
*/
|
|
@@ -489,76 +1065,84 @@ var localizedToolError = async ({
|
|
|
489
1065
|
error,
|
|
490
1066
|
i18n: await i18n.catalog.getI18n(requested)
|
|
491
1067
|
});
|
|
492
|
-
return rendered ? toolError(
|
|
1068
|
+
return rendered ? toolError({
|
|
1069
|
+
message: rendered.message,
|
|
1070
|
+
code: rendered.code
|
|
1071
|
+
}) : void 0;
|
|
493
1072
|
} catch {
|
|
494
1073
|
return;
|
|
495
1074
|
}
|
|
496
1075
|
};
|
|
497
1076
|
/**
|
|
498
|
-
*
|
|
499
|
-
*
|
|
1077
|
+
* The package's `checkScopes` helper is not reused here because it throws
|
|
1078
|
+
* rather than returning an `isError` result, and it reads identity from
|
|
1079
|
+
* context itself.
|
|
1080
|
+
*/
|
|
1081
|
+
var hasScope = ({
|
|
1082
|
+
identity,
|
|
1083
|
+
requiredScope
|
|
1084
|
+
}) => {
|
|
1085
|
+
return (Array.isArray(identity.scopes) ? identity.scopes : []).includes(requiredScope);
|
|
1086
|
+
};
|
|
1087
|
+
/**
|
|
1088
|
+
* Factory for a gate: a function that wraps a {@link Tool} so every call
|
|
1089
|
+
* passes the same authentication and authorization pipeline before its
|
|
1090
|
+
* handler runs. The gated tool is an ordinary `Tool`, so it registers
|
|
1091
|
+
* directly or joins a catalog like any other.
|
|
500
1092
|
*
|
|
501
|
-
* Every
|
|
1093
|
+
* Every gated call:
|
|
502
1094
|
* 1. Resolves the caller identity (default: `getIdentity()`). Returns an
|
|
503
1095
|
* `isError` result when the identity is absent (unauthenticated request).
|
|
504
1096
|
* 2. When `enforceScope` is `true` (default), checks `requiredScope` — returns
|
|
505
1097
|
* an `isError` result when the scope is absent, consistent with how MCP
|
|
506
|
-
* surfaces authorization errors.
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
* 3. Runs global `gates` then per-`register` `def.gates` in order; a throwing
|
|
510
|
-
* gate rejects the call. Every gate receives the full {@link ToolCallContext}
|
|
511
|
-
* (identity **and** the validated call args), enabling arg-conditional gates.
|
|
512
|
-
* Gates own their own error handling; `onError` covers the tool handler only.
|
|
1098
|
+
* surfaces authorization errors.
|
|
1099
|
+
* 3. Runs global `gates` then the tool's `gates` in order; a throwing gate
|
|
1100
|
+
* rejects the call. Every gate receives the full {@link ToolCallContext}.
|
|
513
1101
|
* 4. Merges `buildContext` output into the handler args.
|
|
514
|
-
* 5.
|
|
515
|
-
* 6.
|
|
516
|
-
* 7. With `i18n`, renders a `LocalizedError` from a gate or the handler as an
|
|
1102
|
+
* 5. Calls `onError` on handler throw before rethrowing.
|
|
1103
|
+
* 6. With `i18n`, renders a `LocalizedError` from a gate or the handler as an
|
|
517
1104
|
* `isError` result in the caller's locale instead of rethrowing it.
|
|
518
1105
|
*
|
|
519
1106
|
* @example
|
|
520
1107
|
* ```typescript
|
|
521
|
-
* const
|
|
522
|
-
* server,
|
|
1108
|
+
* const gate = createToolGate({
|
|
523
1109
|
* resolveIdentity: () => {
|
|
524
1110
|
* const jwt = getIdentity<{ sub: string; scopes: string[] }>();
|
|
525
1111
|
* return { userId: jwt!.sub, scopes: jwt!.scopes };
|
|
526
1112
|
* },
|
|
527
1113
|
* });
|
|
528
1114
|
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
1115
|
+
* registerTools({
|
|
1116
|
+
* server,
|
|
1117
|
+
* tools: [
|
|
1118
|
+
* gate({
|
|
1119
|
+
* requiredScope: 'campaigns:write',
|
|
1120
|
+
* // arg-conditional gate: active vs. inactive have different restrictions
|
|
1121
|
+
* gates: [
|
|
1122
|
+
* ({ args }) =>
|
|
1123
|
+
* args.isActive
|
|
1124
|
+
* ? subscriptionGate(['mustNotExceedMaxActive', 'mustHaveBudget'])
|
|
1125
|
+
* : subscriptionGate(['mustIncludeService']),
|
|
1126
|
+
* ],
|
|
1127
|
+
* tool: defineTool({
|
|
1128
|
+
* name: 'activate-campaign',
|
|
1129
|
+
* description: 'Activate or deactivate a campaign.',
|
|
1130
|
+
* inputSchema: z.object({ isActive: z.boolean() }),
|
|
1131
|
+
* method: ({ userId, isActive }) => toggleCampaign(userId, isActive),
|
|
1132
|
+
* }),
|
|
1133
|
+
* }),
|
|
540
1134
|
* ],
|
|
541
|
-
* method: async ({ userId, isActive }) => toggleCampaign(userId, isActive),
|
|
542
1135
|
* });
|
|
543
1136
|
* ```
|
|
544
1137
|
*/
|
|
545
|
-
var
|
|
546
|
-
if (!(Array.isArray(identity.scopes) ? identity.scopes : []).includes(requiredScope)) return toolError(`This tool requires the "${requiredScope}" scope.`);
|
|
547
|
-
return null;
|
|
548
|
-
};
|
|
549
|
-
var runGates = async (allGates, ctx) => {
|
|
550
|
-
for (const gate of allGates) await gate(ctx);
|
|
551
|
-
};
|
|
552
|
-
var createGatedToolRegistrar = ({
|
|
553
|
-
server,
|
|
1138
|
+
var createToolGate = ({
|
|
554
1139
|
resolveIdentity = getIdentity,
|
|
555
1140
|
gates = [],
|
|
556
1141
|
enforceScope = true,
|
|
557
1142
|
onError,
|
|
558
1143
|
buildContext,
|
|
559
|
-
notFoundMessage = "Not found",
|
|
560
1144
|
i18n
|
|
561
|
-
}) => {
|
|
1145
|
+
} = {}) => {
|
|
562
1146
|
const renderOrRethrow = async (error, ctx) => {
|
|
563
1147
|
const localized = i18n ? await localizedToolError({
|
|
564
1148
|
error,
|
|
@@ -568,60 +1152,46 @@ var createGatedToolRegistrar = ({
|
|
|
568
1152
|
if (localized) return localized;
|
|
569
1153
|
throw error;
|
|
570
1154
|
};
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
} catch (error) {
|
|
610
|
-
return renderOrRethrow(error, ctx);
|
|
1155
|
+
return ({
|
|
1156
|
+
tool,
|
|
1157
|
+
requiredScope,
|
|
1158
|
+
gates: toolGates = []
|
|
1159
|
+
}) => {
|
|
1160
|
+
return {
|
|
1161
|
+
...tool,
|
|
1162
|
+
handler: async args => {
|
|
1163
|
+
const identity = resolveIdentity();
|
|
1164
|
+
if (!identity) return toolError({
|
|
1165
|
+
message: "Unauthorized"
|
|
1166
|
+
});
|
|
1167
|
+
const ctx = {
|
|
1168
|
+
identity,
|
|
1169
|
+
args,
|
|
1170
|
+
handler: tool.name
|
|
1171
|
+
};
|
|
1172
|
+
if (enforceScope && !hasScope({
|
|
1173
|
+
identity,
|
|
1174
|
+
requiredScope
|
|
1175
|
+
})) return toolError({
|
|
1176
|
+
message: `This tool requires the "${requiredScope}" scope.`
|
|
1177
|
+
});
|
|
1178
|
+
try {
|
|
1179
|
+
for (const gate of [...gates, ...toolGates]) await gate(ctx);
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
return renderOrRethrow(error, ctx);
|
|
1182
|
+
}
|
|
1183
|
+
const extra = buildContext ? buildContext(ctx) : {};
|
|
1184
|
+
try {
|
|
1185
|
+
return await tool.handler({
|
|
1186
|
+
...args,
|
|
1187
|
+
...extra
|
|
1188
|
+
});
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
if (onError) await onError(error, ctx);
|
|
1191
|
+
return renderOrRethrow(error, ctx);
|
|
1192
|
+
}
|
|
611
1193
|
}
|
|
612
|
-
return runMethod(def, ctx);
|
|
613
1194
|
};
|
|
614
|
-
server.registerTool(def.name, {
|
|
615
|
-
title: def.title,
|
|
616
|
-
description: def.description,
|
|
617
|
-
inputSchema: def.inputSchema,
|
|
618
|
-
outputSchema: def.outputSchema,
|
|
619
|
-
annotations: def.annotations,
|
|
620
|
-
_meta: def._meta
|
|
621
|
-
}, handler);
|
|
622
|
-
};
|
|
623
|
-
return {
|
|
624
|
-
register
|
|
625
1195
|
};
|
|
626
1196
|
};
|
|
627
1197
|
|
|
@@ -1010,14 +1580,16 @@ exports.UI_EXTENSION_ID = UI_EXTENSION_ID;
|
|
|
1010
1580
|
exports.UI_RESOURCE_MIME_TYPE = UI_RESOURCE_MIME_TYPE;
|
|
1011
1581
|
exports.apiCall = apiCall;
|
|
1012
1582
|
exports.checkScopes = checkScopes;
|
|
1013
|
-
exports.createGatedToolRegistrar = createGatedToolRegistrar;
|
|
1014
1583
|
exports.createMcpRouter = createMcpRouter;
|
|
1584
|
+
exports.createToolGate = createToolGate;
|
|
1585
|
+
exports.defineTool = defineTool;
|
|
1015
1586
|
exports.errorBodyMessage = errorBodyMessage;
|
|
1016
1587
|
exports.getApiHeaders = getApiHeaders;
|
|
1017
1588
|
exports.getIdentity = getIdentity;
|
|
1018
1589
|
exports.getRequestLocale = getRequestLocale;
|
|
1590
|
+
exports.rankTools = rankTools;
|
|
1019
1591
|
exports.registerAppResource = registerAppResource;
|
|
1020
|
-
exports.
|
|
1592
|
+
exports.registerTools = registerTools;
|
|
1021
1593
|
Object.defineProperty(exports, 'z', {
|
|
1022
1594
|
enumerable: true,
|
|
1023
1595
|
get: function () {
|