toolnexus 0.20.0 → 0.22.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/README.md +40 -0
- package/dist/agents/agent.d.ts +12 -1
- package/dist/agents/agent.js +43 -7
- package/dist/agents/compaction.d.ts +19 -0
- package/dist/agents/compaction.js +26 -4
- package/dist/agents/runtime.d.ts +5 -0
- package/dist/agents/runtime.js +13 -2
- package/dist/batteries.d.ts +223 -0
- package/dist/batteries.js +488 -0
- package/dist/builtin.d.ts +16 -0
- package/dist/builtin.js +315 -17
- package/dist/builtin_extras.d.ts +80 -0
- package/dist/builtin_extras.js +248 -0
- package/dist/classifier.d.ts +14 -0
- package/dist/classifier.js +34 -1
- package/dist/client.d.ts +91 -15
- package/dist/client.js +377 -110
- package/dist/index.d.ts +8 -0
- package/dist/index.js +11 -0
- package/dist/judge.d.ts +98 -0
- package/dist/judge.js +199 -0
- package/dist/mcp.d.ts +32 -1
- package/dist/mcp.js +184 -6
- package/dist/models.d.ts +20 -0
- package/dist/models.js +46 -0
- package/dist/promptcache.d.ts +21 -0
- package/dist/promptcache.js +59 -0
- package/dist/store.d.ts +32 -0
- package/dist/store.js +131 -0
- package/dist/tooltiers.d.ts +49 -0
- package/dist/tooltiers.js +197 -0
- package/dist/types.d.ts +19 -0
- package/package.json +2 -2
- package/src/agents/agent.ts +39 -8
- package/src/agents/compaction.ts +45 -4
- package/src/agents/runtime.ts +16 -2
- package/src/batteries.ts +614 -0
- package/src/builtin.ts +333 -18
- package/src/builtin_extras.ts +285 -0
- package/src/classifier.ts +36 -1
- package/src/client.ts +415 -115
- package/src/index.ts +31 -0
- package/src/judge.ts +305 -0
- package/src/mcp.ts +204 -7
- package/src/models.ts +56 -0
- package/src/promptcache.ts +63 -0
- package/src/store.ts +142 -0
- package/src/tooltiers.ts +221 -0
- package/src/types.ts +20 -0
package/dist/index.d.ts
CHANGED
|
@@ -14,5 +14,13 @@ export * from "./adapters.js";
|
|
|
14
14
|
export * from "./translate.js";
|
|
15
15
|
export * from "./toolkit.js";
|
|
16
16
|
export * from "./client.js";
|
|
17
|
+
export * from "./store.js";
|
|
17
18
|
export * from "./classifier.js";
|
|
18
19
|
export * as agents from "./agents/index.js";
|
|
20
|
+
export * as judge from "./judge.js";
|
|
21
|
+
export * from "./batteries.js";
|
|
22
|
+
export { State, context, questionMap, ask, gate, decide, applyGate, applyPolicy, answersOf, staticClassifier, Tape, DEFAULT_BANDS, type NamedQuestion, type Band, type Bands, type Answers, type Rule, type GateOutcome, type Policy, } from "./judge.js";
|
|
23
|
+
export * from "./promptcache.js";
|
|
24
|
+
export * from "./tooltiers.js";
|
|
25
|
+
export * from "./models.js";
|
|
26
|
+
export * from "./builtin_extras.js";
|
package/dist/index.js
CHANGED
|
@@ -14,5 +14,16 @@ export * from "./adapters.js";
|
|
|
14
14
|
export * from "./translate.js";
|
|
15
15
|
export * from "./toolkit.js";
|
|
16
16
|
export * from "./client.js";
|
|
17
|
+
export * from "./store.js";
|
|
17
18
|
export * from "./classifier.js";
|
|
18
19
|
export * as agents from "./agents/index.js";
|
|
20
|
+
// Simple judgments (§8B, add-judge-adapters). `judge.noul/choice/score` are the NAMED builders
|
|
21
|
+
// (the bare `noul/choice/score` above stay the §8B wire builders); the rest is also top-level.
|
|
22
|
+
export * as judge from "./judge.js";
|
|
23
|
+
// Judge batteries (§8B "Batteries", add-judge-batteries): eight `*Classifier` values + latestUserText.
|
|
24
|
+
export * from "./batteries.js";
|
|
25
|
+
export { State, context, questionMap, ask, gate, decide, applyGate, applyPolicy, answersOf, staticClassifier, Tape, DEFAULT_BANDS, } from "./judge.js";
|
|
26
|
+
export * from "./promptcache.js";
|
|
27
|
+
export * from "./tooltiers.js";
|
|
28
|
+
export * from "./models.js";
|
|
29
|
+
export * from "./builtin_extras.js";
|
package/dist/judge.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple judgments (SPEC.md §8B "Simple judgments — ask / gate", change `add-judge-adapters`).
|
|
3
|
+
* A thin layer over any `Classifier`: named question builders, a role-carrying `State`,
|
|
4
|
+
* `ask` (answers by name with bands) and `gate` / `Policy` (rules that escalate instead of
|
|
5
|
+
* deciding when unsure). The wire is unchanged — the builders produce exactly the §8B
|
|
6
|
+
* `evaluate(state, questions)` inputs.
|
|
7
|
+
*
|
|
8
|
+
* A gate never authorises; it only declines to decide.
|
|
9
|
+
*/
|
|
10
|
+
import { Classifier, Decision, type DecisionAnswer, type NoulCriteria, type Question, type RecordedDecision } from "./classifier.js";
|
|
11
|
+
import type { Request } from "./types.js";
|
|
12
|
+
/** One named question of an ordered question list. */
|
|
13
|
+
export interface NamedQuestion {
|
|
14
|
+
name: string;
|
|
15
|
+
question: Question;
|
|
16
|
+
}
|
|
17
|
+
/** `noul` question with a name. Name the state field it judges in `instructions`. */
|
|
18
|
+
export declare function noul(name: string, instructions: string, criteria?: NoulCriteria): NamedQuestion;
|
|
19
|
+
/** `choice` question with a name; `options` maps option -> what picking it MEANS. */
|
|
20
|
+
export declare function choice(name: string, instructions: string, options: Record<string, string>): NamedQuestion;
|
|
21
|
+
/** `score` question with a name; `levels` is the ordered rubric. */
|
|
22
|
+
export declare function score(name: string, instructions: string, levels: string[]): NamedQuestion;
|
|
23
|
+
/** Ordered list -> the §8B questions map. A duplicate name is an error naming it. */
|
|
24
|
+
export declare function questionMap(questions: readonly NamedQuestion[]): Record<string, Question>;
|
|
25
|
+
/** `role` next to the data's fields; a non-object value goes under `data`. */
|
|
26
|
+
export declare function State(role: string, data: unknown): Record<string, unknown>;
|
|
27
|
+
/** Sugar: `{context, message, ...extra}`. */
|
|
28
|
+
export declare function context(ctx: string, message: string, extra?: Record<string, unknown>): Record<string, unknown>;
|
|
29
|
+
export type Band = "yes" | "no" | "uncertain";
|
|
30
|
+
export interface Bands {
|
|
31
|
+
low: number;
|
|
32
|
+
high: number;
|
|
33
|
+
}
|
|
34
|
+
/** Default cut-points; exclusive on the confident side. */
|
|
35
|
+
export declare const DEFAULT_BANDS: Readonly<Bands>;
|
|
36
|
+
/** One answer from `ask`: the §8B answer plus `band` (noul) or `sure` (choice/score). */
|
|
37
|
+
export type Answer = DecisionAnswer & {
|
|
38
|
+
/** noul only. */
|
|
39
|
+
band?: Band;
|
|
40
|
+
/** choice / score only. */
|
|
41
|
+
sure?: boolean;
|
|
42
|
+
/** The one number: noul probability, score value, or choice confidence. */
|
|
43
|
+
value(): number;
|
|
44
|
+
/** The picked option of a choice answer ("" otherwise). JS keeps §8B's `choice` string FIELD on a
|
|
45
|
+
* choice answer (a method of the same name would shadow it), so this is `pick()`. */
|
|
46
|
+
pick(): string;
|
|
47
|
+
};
|
|
48
|
+
export type Answers = Record<string, Answer>;
|
|
49
|
+
/** Band / sure every answer of a decision. */
|
|
50
|
+
export declare function answersOf(decision: Decision, bands?: Bands): Answers;
|
|
51
|
+
/** Evaluate once; answers by name, each with a band (noul) or `sure` (choice/score). */
|
|
52
|
+
export declare function ask(classifier: Classifier, state: unknown, questions: readonly NamedQuestion[], bands?: Bands): Promise<Answers>;
|
|
53
|
+
export interface Rule {
|
|
54
|
+
question: string;
|
|
55
|
+
below?: number;
|
|
56
|
+
at_least?: number;
|
|
57
|
+
is?: string;
|
|
58
|
+
action: string;
|
|
59
|
+
target?: string;
|
|
60
|
+
}
|
|
61
|
+
export interface GateOutcome {
|
|
62
|
+
action: string;
|
|
63
|
+
target: string;
|
|
64
|
+
escalated: boolean;
|
|
65
|
+
/** Present iff escalated: a §10 `input` Request. */
|
|
66
|
+
request?: Request;
|
|
67
|
+
answers: Answers;
|
|
68
|
+
}
|
|
69
|
+
export interface Policy {
|
|
70
|
+
rules: Rule[];
|
|
71
|
+
/** The no-rule-fired action; empty escalates with reason "no rule fired". */
|
|
72
|
+
default?: string;
|
|
73
|
+
bands?: Bands;
|
|
74
|
+
/** Skip a rule whose answer is uncertain instead of escalating on it. */
|
|
75
|
+
skipUncertain?: boolean;
|
|
76
|
+
}
|
|
77
|
+
/** Pure half of `gate`: first-match rules; an unsure or missing answer on rule i escalates and wins. */
|
|
78
|
+
export declare function applyGate(answers: Answers, rules: readonly Rule[]): GateOutcome;
|
|
79
|
+
/** Pure half of a `Policy`. */
|
|
80
|
+
export declare function applyPolicy(answers: Answers, policy: Policy): GateOutcome;
|
|
81
|
+
/** Ask, then apply rules; no rule fired -> empty action (use a `Policy` for a declared default). */
|
|
82
|
+
export declare function gate(classifier: Classifier, state: unknown, questions: readonly NamedQuestion[], rules: readonly Rule[], bands?: Bands): Promise<GateOutcome>;
|
|
83
|
+
/** Ask, then apply a `Policy` (rules, default, bands, skipUncertain). */
|
|
84
|
+
export declare function decide(classifier: Classifier, state: unknown, questions: readonly NamedQuestion[], policy: Policy): Promise<GateOutcome>;
|
|
85
|
+
/** One-line `static` classifier from recorded decisions. */
|
|
86
|
+
export declare function staticClassifier(decisions: RecordedDecision[] | RecordedDecision, model?: string): Classifier;
|
|
87
|
+
/**
|
|
88
|
+
* Records live decisions keyed by a caller-given call name, and replays them with no network.
|
|
89
|
+
* `tape.classifier(name)` returns a Classifier: recording (live given) or replaying (no live).
|
|
90
|
+
*/
|
|
91
|
+
export declare class Tape {
|
|
92
|
+
private readonly live?;
|
|
93
|
+
readonly entries: Record<string, RecordedDecision>;
|
|
94
|
+
constructor(live?: Classifier | undefined, entries?: Record<string, RecordedDecision>);
|
|
95
|
+
/** A replay-only tape from saved entries. */
|
|
96
|
+
static replay(entries: Record<string, RecordedDecision>): Tape;
|
|
97
|
+
classifier(name: string): Classifier;
|
|
98
|
+
}
|
package/dist/judge.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple judgments (SPEC.md §8B "Simple judgments — ask / gate", change `add-judge-adapters`).
|
|
3
|
+
* A thin layer over any `Classifier`: named question builders, a role-carrying `State`,
|
|
4
|
+
* `ask` (answers by name with bands) and `gate` / `Policy` (rules that escalate instead of
|
|
5
|
+
* deciding when unsure). The wire is unchanged — the builders produce exactly the §8B
|
|
6
|
+
* `evaluate(state, questions)` inputs.
|
|
7
|
+
*
|
|
8
|
+
* A gate never authorises; it only declines to decide.
|
|
9
|
+
*/
|
|
10
|
+
import { Classifier, noul as noulQ, choice as choiceQ, score as scoreQ, } from "./classifier.js";
|
|
11
|
+
/** `noul` question with a name. Name the state field it judges in `instructions`. */
|
|
12
|
+
export function noul(name, instructions, criteria) {
|
|
13
|
+
return { name, question: noulQ(instructions, criteria) };
|
|
14
|
+
}
|
|
15
|
+
/** `choice` question with a name; `options` maps option -> what picking it MEANS. */
|
|
16
|
+
export function choice(name, instructions, options) {
|
|
17
|
+
return { name, question: choiceQ(instructions, options) };
|
|
18
|
+
}
|
|
19
|
+
/** `score` question with a name; `levels` is the ordered rubric. */
|
|
20
|
+
export function score(name, instructions, levels) {
|
|
21
|
+
return { name, question: scoreQ(instructions, levels) };
|
|
22
|
+
}
|
|
23
|
+
/** Ordered list -> the §8B questions map. A duplicate name is an error naming it. */
|
|
24
|
+
export function questionMap(questions) {
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const { name, question } of questions) {
|
|
27
|
+
if (Object.prototype.hasOwnProperty.call(out, name))
|
|
28
|
+
throw new Error(`duplicate question name ${JSON.stringify(name)}`);
|
|
29
|
+
out[name] = question;
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
// ---------------------------------------------------------------- state
|
|
34
|
+
/** `role` next to the data's fields; a non-object value goes under `data`. */
|
|
35
|
+
export function State(role, data) {
|
|
36
|
+
if (data !== null && typeof data === "object" && !Array.isArray(data))
|
|
37
|
+
return { role, ...data };
|
|
38
|
+
return { role, data };
|
|
39
|
+
}
|
|
40
|
+
/** Sugar: `{context, message, ...extra}`. */
|
|
41
|
+
export function context(ctx, message, extra = {}) {
|
|
42
|
+
return { context: ctx, message, ...extra };
|
|
43
|
+
}
|
|
44
|
+
/** Default cut-points; exclusive on the confident side. */
|
|
45
|
+
export const DEFAULT_BANDS = Object.freeze({ low: 0.3, high: 0.7 });
|
|
46
|
+
function noulBand(p, b) {
|
|
47
|
+
return p < b.low ? "no" : p > b.high ? "yes" : "uncertain";
|
|
48
|
+
}
|
|
49
|
+
function wrap(a, b) {
|
|
50
|
+
const extra = a.type === "noul"
|
|
51
|
+
? { band: noulBand(a.noul, b) }
|
|
52
|
+
: { sure: a.confidence > b.high && !(a.type === "choice" && a.nearUniform) };
|
|
53
|
+
const value = () => (a.type === "noul" ? a.noul : a.type === "score" ? a.score : a.confidence);
|
|
54
|
+
const pick = () => (a.type === "choice" ? a.choice : "");
|
|
55
|
+
const out = { ...a, ...extra };
|
|
56
|
+
Object.defineProperty(out, "value", { value, enumerable: false });
|
|
57
|
+
Object.defineProperty(out, "pick", { value: pick, enumerable: false });
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/** Band / sure every answer of a decision. */
|
|
61
|
+
export function answersOf(decision, bands = DEFAULT_BANDS) {
|
|
62
|
+
return Object.fromEntries(Object.entries(decision.answers).map(([k, a]) => [k, wrap(a, bands)]));
|
|
63
|
+
}
|
|
64
|
+
/** Evaluate once; answers by name, each with a band (noul) or `sure` (choice/score). */
|
|
65
|
+
export async function ask(classifier, state, questions, bands = DEFAULT_BANDS) {
|
|
66
|
+
return answersOf(await classifier.evaluate(state, questionMap(questions)), bands);
|
|
67
|
+
}
|
|
68
|
+
function unsure(a) {
|
|
69
|
+
return a.type === "noul" ? a.band === "uncertain" : !a.sure;
|
|
70
|
+
}
|
|
71
|
+
function escalate(answers, id, question, reason, prompt) {
|
|
72
|
+
return {
|
|
73
|
+
action: "needs_input",
|
|
74
|
+
target: "",
|
|
75
|
+
escalated: true,
|
|
76
|
+
answers,
|
|
77
|
+
request: { id, kind: "input", prompt, data: { question, reason, answers } },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function applyRules(answers, rules, skipUncertain) {
|
|
81
|
+
for (const [i, r] of rules.entries()) {
|
|
82
|
+
const a = answers[r.question];
|
|
83
|
+
if (!a || unsure(a)) {
|
|
84
|
+
if (a && skipUncertain)
|
|
85
|
+
continue;
|
|
86
|
+
const reason = `${a ? "uncertain" : "missing"} answer "${r.question}"`;
|
|
87
|
+
return escalate(answers, `gate:${i}:${r.question}`, r.question, reason, `Classifier is unsure about "${r.question}" (${reason}). Decide rule ${i} (${r.action}).`);
|
|
88
|
+
}
|
|
89
|
+
const key = JSON.stringify(r.question);
|
|
90
|
+
let fired = false;
|
|
91
|
+
let misfit = "";
|
|
92
|
+
if (r.is !== undefined) {
|
|
93
|
+
if (a.type === "choice")
|
|
94
|
+
fired = a.choice === r.is;
|
|
95
|
+
else
|
|
96
|
+
misfit = `is-rule on ${a.type} answer ${key}`;
|
|
97
|
+
}
|
|
98
|
+
else if (a.type === "choice") {
|
|
99
|
+
misfit = `numeric rule on choice answer ${key}`;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
const v = a.type === "noul" ? a.noul : a.score;
|
|
103
|
+
if (r.below !== undefined)
|
|
104
|
+
fired = v < r.below;
|
|
105
|
+
else if (r.at_least !== undefined)
|
|
106
|
+
fired = v >= r.at_least;
|
|
107
|
+
else
|
|
108
|
+
misfit = `rule on ${key} has no below / at_least / is`;
|
|
109
|
+
}
|
|
110
|
+
// A rule that does not fit its answer escalates; it is never skipped.
|
|
111
|
+
if (misfit)
|
|
112
|
+
return escalate(answers, `gate:${i}:${r.question}`, r.question, misfit, `Rule ${i} (${r.action}) does not fit its answer (${misfit}). Decide.`);
|
|
113
|
+
if (fired)
|
|
114
|
+
return { action: r.action, target: r.target ?? "", escalated: false, answers };
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
/** Pure half of `gate`: first-match rules; an unsure or missing answer on rule i escalates and wins. */
|
|
119
|
+
export function applyGate(answers, rules) {
|
|
120
|
+
return applyRules(answers, rules, false) ?? { action: "", target: "", escalated: false, answers };
|
|
121
|
+
}
|
|
122
|
+
/** Pure half of a `Policy`. */
|
|
123
|
+
export function applyPolicy(answers, policy) {
|
|
124
|
+
const hit = applyRules(answers, policy.rules, policy.skipUncertain ?? false);
|
|
125
|
+
if (hit)
|
|
126
|
+
return hit;
|
|
127
|
+
if (policy.default)
|
|
128
|
+
return { action: policy.default, target: "", escalated: false, answers };
|
|
129
|
+
return escalate(answers, "gate:default", "", "no rule fired", "No rule fired. Decide the action.");
|
|
130
|
+
}
|
|
131
|
+
/** Ask, then apply rules; no rule fired -> empty action (use a `Policy` for a declared default). */
|
|
132
|
+
export async function gate(classifier, state, questions, rules, bands = DEFAULT_BANDS) {
|
|
133
|
+
return applyGate(await ask(classifier, state, questions, bands), rules);
|
|
134
|
+
}
|
|
135
|
+
/** Ask, then apply a `Policy` (rules, default, bands, skipUncertain). */
|
|
136
|
+
export async function decide(classifier, state, questions, policy) {
|
|
137
|
+
return applyPolicy(await ask(classifier, state, questions, policy.bands ?? DEFAULT_BANDS), policy);
|
|
138
|
+
}
|
|
139
|
+
// ---------------------------------------------------------------- static + tape
|
|
140
|
+
/** One-line `static` classifier from recorded decisions. */
|
|
141
|
+
export function staticClassifier(decisions, model) {
|
|
142
|
+
return new Classifier({ style: "static", decisions: Array.isArray(decisions) ? decisions : [decisions], model });
|
|
143
|
+
}
|
|
144
|
+
/** The recorded-body form of a Decision, decodable by `static`. */
|
|
145
|
+
function toRaw(d) {
|
|
146
|
+
const answers = {};
|
|
147
|
+
for (const [k, a] of Object.entries(d.answers)) {
|
|
148
|
+
if (a.type === "choice") {
|
|
149
|
+
const { nearUniform: _derived, ...rest } = a;
|
|
150
|
+
answers[k] = rest;
|
|
151
|
+
}
|
|
152
|
+
else
|
|
153
|
+
answers[k] = { ...a };
|
|
154
|
+
}
|
|
155
|
+
const usage = { input_tokens: d.usage.inputTokens, output_tokens: d.usage.outputTokens };
|
|
156
|
+
if (d.usage.cost !== undefined)
|
|
157
|
+
usage.cost = d.usage.cost;
|
|
158
|
+
return { model: d.model, answers, usage, calibrated: d.calibrated };
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Records live decisions keyed by a caller-given call name, and replays them with no network.
|
|
162
|
+
* `tape.classifier(name)` returns a Classifier: recording (live given) or replaying (no live).
|
|
163
|
+
*/
|
|
164
|
+
export class Tape {
|
|
165
|
+
live;
|
|
166
|
+
entries;
|
|
167
|
+
constructor(live, entries = {}) {
|
|
168
|
+
this.live = live;
|
|
169
|
+
this.entries = { ...entries };
|
|
170
|
+
}
|
|
171
|
+
/** A replay-only tape from saved entries. */
|
|
172
|
+
static replay(entries) {
|
|
173
|
+
return new Tape(undefined, entries);
|
|
174
|
+
}
|
|
175
|
+
classifier(name) {
|
|
176
|
+
const live = this.live;
|
|
177
|
+
if (live) {
|
|
178
|
+
return new Classifier({
|
|
179
|
+
style: "custom",
|
|
180
|
+
evaluate: async (state, questions, signal) => {
|
|
181
|
+
const d = await live.evaluate(state, questions, signal);
|
|
182
|
+
this.entries[name] = { state, questions, response: toRaw(d) };
|
|
183
|
+
return d;
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// Replay: resolved when evaluated (a miss fails then, naming the key, and sends nothing).
|
|
188
|
+
return new Classifier({
|
|
189
|
+
style: "custom",
|
|
190
|
+
evaluate: async (state, questions, signal) => {
|
|
191
|
+
const rec = this.entries[name];
|
|
192
|
+
if (!rec)
|
|
193
|
+
throw new Error(`tape: no recorded decision for call "${name}"`);
|
|
194
|
+
const raw = rec.response;
|
|
195
|
+
return staticClassifier(rec, raw?.model).evaluate(state, questions, signal);
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ElicitRequest, ElicitResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
-
import { type JSONSchema, type Tool, type McpStatus, type Request, type Answer } from "./types.js";
|
|
2
|
+
import { type JSONSchema, type Tool, type ToolHints, type McpStatus, type Request, type Answer } from "./types.js";
|
|
3
3
|
import { type ContentPart } from "./content.js";
|
|
4
4
|
/** Map an MCP `elicitation/create` request onto a §10 `Request`. */
|
|
5
5
|
export declare function elicitationToRequest(params: ElicitRequest["params"]): Request;
|
|
@@ -50,13 +50,44 @@ export interface McpSource {
|
|
|
50
50
|
tools: Tool[];
|
|
51
51
|
status: Record<string, McpStatus>;
|
|
52
52
|
close(): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* `list_mcp_resources` and `read_mcp_resource` bound to this source's connected servers
|
|
55
|
+
* (SPEC §4C). Both read-only. Opt-in: register them on a toolkit. They stop working once the
|
|
56
|
+
* source is closed.
|
|
57
|
+
*/
|
|
58
|
+
resourceTools(): Tool[];
|
|
53
59
|
}
|
|
60
|
+
/** One server's outcome, reported by `loadMcp`'s `onServer` the moment that server finishes. */
|
|
61
|
+
export interface McpServerResult {
|
|
62
|
+
name: string;
|
|
63
|
+
status: McpStatus;
|
|
64
|
+
/** This server's converted tools (empty unless connected). Usable until the source closes. */
|
|
65
|
+
tools: Tool[];
|
|
66
|
+
/** Why the server failed (absent when connected or disabled). */
|
|
67
|
+
error?: Error;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* MCP tool annotations → ToolHints (SPEC §4C). No hint fields at all ⇒ undefined (unknown).
|
|
71
|
+
* Defaults follow the MCP spec: readOnlyHint false, destructiveHint and openWorldHint true.
|
|
72
|
+
*/
|
|
73
|
+
export declare function mcpHints(a: {
|
|
74
|
+
readOnlyHint?: boolean;
|
|
75
|
+
destructiveHint?: boolean;
|
|
76
|
+
openWorldHint?: boolean;
|
|
77
|
+
} | undefined): ToolHints | undefined;
|
|
54
78
|
/** Connect to every enabled server, list + convert tools. Failures are isolated.
|
|
55
79
|
* §2 Gap 3: an optional `signal` bounds each connect/list by the server timeout and aborts the
|
|
56
80
|
* whole load promptly on parent cancellation. */
|
|
57
81
|
export declare function loadMcp(input: string | object, opts?: {
|
|
58
82
|
waitFor?: (request: Request) => Promise<Answer>;
|
|
59
83
|
signal?: AbortSignal;
|
|
84
|
+
/**
|
|
85
|
+
* Called once per server the moment it is connected, failed or found disabled (SPEC §4C),
|
|
86
|
+
* so a host can register a fast server's tools while a slow one is still connecting.
|
|
87
|
+
* Calls are serialized (an async callback is awaited before the next one runs); the load
|
|
88
|
+
* still resolves only after every server and every callback has finished.
|
|
89
|
+
*/
|
|
90
|
+
onServer?: (result: McpServerResult) => void | Promise<void>;
|
|
60
91
|
}): Promise<McpSource>;
|
|
61
92
|
/** §2 Gap 6. One listed tool definition (original, unprefixed name). */
|
|
62
93
|
export interface ToolInfo {
|
package/dist/mcp.js
CHANGED
|
@@ -59,12 +59,36 @@ export function parseMcpConfig(input) {
|
|
|
59
59
|
const raw = typeof input === "string" ? JSON.parse(readFileSync(input, "utf8")) : input;
|
|
60
60
|
const wrapped = raw.mcpServers ?? raw.servers ?? raw.mcp;
|
|
61
61
|
if (wrapped !== undefined)
|
|
62
|
-
return wrapped;
|
|
62
|
+
return normalizeServers(wrapped);
|
|
63
63
|
// Bare map of servers — but strip sibling top-level config keys (builtins/
|
|
64
64
|
// agents/a2a/mcpServer) so they are not mistaken for MCP servers when no
|
|
65
65
|
// wrapper key is present.
|
|
66
66
|
const { builtins: _builtins, agents: _agents, a2a: _a2a, mcpServer: _mcpServer, ...servers } = raw;
|
|
67
|
-
return servers;
|
|
67
|
+
return normalizeServers(servers);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* §2 `command` + `args`. A local server's `command` may be a non-empty string or a non-empty
|
|
71
|
+
* string array; an optional `args` string array is appended. The result is always the argv form
|
|
72
|
+
* (`command: string[]`, no `args`). Spawned with no shell. A remote server ignores `args`.
|
|
73
|
+
*/
|
|
74
|
+
function normalizeServers(servers) {
|
|
75
|
+
const out = {};
|
|
76
|
+
for (const [name, cfg] of Object.entries(servers ?? {})) {
|
|
77
|
+
if (cfg === null || typeof cfg !== "object" || isRemote(cfg)) {
|
|
78
|
+
out[name] = cfg;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const { args, command, ...rest } = cfg;
|
|
82
|
+
const head = typeof command === "string" ? [command] : Array.isArray(command) ? command : undefined;
|
|
83
|
+
if (!head || head.length === 0 || head[0] === "" || head.some((c) => typeof c !== "string")) {
|
|
84
|
+
throw new Error(`mcp server "${name}": invalid command`);
|
|
85
|
+
}
|
|
86
|
+
if (args !== undefined && (!Array.isArray(args) || args.some((a) => typeof a !== "string"))) {
|
|
87
|
+
throw new Error(`mcp server "${name}": invalid args`);
|
|
88
|
+
}
|
|
89
|
+
out[name] = { ...rest, command: [...head, ...(args ?? [])] };
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
68
92
|
}
|
|
69
93
|
/**
|
|
70
94
|
* Expand ${ENV_VAR} in header values from process.env so tokens live in the
|
|
@@ -212,9 +236,14 @@ function convertTool(server, def, client, timeout) {
|
|
|
212
236
|
description: def.description ?? "",
|
|
213
237
|
inputSchema,
|
|
214
238
|
source: "mcp",
|
|
239
|
+
...(mcpHints(def.annotations) ? { hints: mcpHints(def.annotations) } : {}),
|
|
215
240
|
async execute(args, ctx) {
|
|
216
241
|
try {
|
|
217
|
-
const result = await client.callTool({ name: def.name, arguments: args ?? {} }, undefined,
|
|
242
|
+
const result = await client.callTool({ name: def.name, arguments: args ?? {} }, undefined,
|
|
243
|
+
// The SDK only attaches `_meta.progressToken` when `onprogress` is supplied; without it
|
|
244
|
+
// `resetTimeoutOnProgress` never fires (add-mcp-progress-timeout). Notifications for a
|
|
245
|
+
// token that matches no in-flight request are dropped by the SDK, so they never reset.
|
|
246
|
+
{ onprogress: () => { }, resetTimeoutOnProgress: true, signal: ctx?.signal, timeout: ctx?.timeout ?? timeout });
|
|
218
247
|
// Parts are collected BEFORE any branch returns: the isError and structuredContent
|
|
219
248
|
// short-circuits both read `content[]` on the way past, so neither reintroduces the
|
|
220
249
|
// silent drop this mapping exists to remove (§2).
|
|
@@ -235,6 +264,120 @@ function convertTool(server, def, client, timeout) {
|
|
|
235
264
|
},
|
|
236
265
|
};
|
|
237
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* MCP tool annotations → ToolHints (SPEC §4C). No hint fields at all ⇒ undefined (unknown).
|
|
269
|
+
* Defaults follow the MCP spec: readOnlyHint false, destructiveHint and openWorldHint true.
|
|
270
|
+
*/
|
|
271
|
+
export function mcpHints(a) {
|
|
272
|
+
if (!a || (a.readOnlyHint === undefined && a.destructiveHint === undefined && a.openWorldHint === undefined))
|
|
273
|
+
return undefined;
|
|
274
|
+
const h = { readOnly: a.readOnlyHint ?? false, destructive: a.destructiveHint ?? true, openWorld: a.openWorldHint ?? true };
|
|
275
|
+
if (h.readOnly)
|
|
276
|
+
h.destructive = false;
|
|
277
|
+
return h;
|
|
278
|
+
}
|
|
279
|
+
function resourceTools(servers) {
|
|
280
|
+
const ro = { readOnly: true, destructive: false, openWorld: false };
|
|
281
|
+
const names = () => [...servers.keys()].sort();
|
|
282
|
+
const conn = (server) => {
|
|
283
|
+
const c = servers.get(server);
|
|
284
|
+
if (!c)
|
|
285
|
+
throw new Error(`no connected MCP server "${server}" (connected: ${names().join(", ")})`);
|
|
286
|
+
return c;
|
|
287
|
+
};
|
|
288
|
+
const list = {
|
|
289
|
+
name: "list_mcp_resources",
|
|
290
|
+
description: "List the resources MCP servers offer (uri, name, description, mimeType). Optionally for one server.",
|
|
291
|
+
inputSchema: {
|
|
292
|
+
type: "object",
|
|
293
|
+
properties: { server: { type: "string", description: "Only this server (default: all)" } },
|
|
294
|
+
additionalProperties: false,
|
|
295
|
+
},
|
|
296
|
+
source: "mcp",
|
|
297
|
+
hints: ro,
|
|
298
|
+
async execute(args, ctx) {
|
|
299
|
+
let targets = names();
|
|
300
|
+
const s = typeof args?.server === "string" ? args.server : "";
|
|
301
|
+
if (s) {
|
|
302
|
+
try {
|
|
303
|
+
conn(s);
|
|
304
|
+
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
return { output: `list_mcp_resources: ${e.message}`, isError: true };
|
|
307
|
+
}
|
|
308
|
+
targets = [s];
|
|
309
|
+
}
|
|
310
|
+
const lines = [];
|
|
311
|
+
const items = [];
|
|
312
|
+
for (const name of targets) {
|
|
313
|
+
const c = conn(name);
|
|
314
|
+
try {
|
|
315
|
+
const res = await c.client.listResources({}, { timeout: c.timeout, signal: ctx?.signal });
|
|
316
|
+
for (const r of res.resources ?? []) {
|
|
317
|
+
let line = `${name} ${r.uri}`;
|
|
318
|
+
if (r.name)
|
|
319
|
+
line += ` — ${r.name}`;
|
|
320
|
+
if (r.mimeType)
|
|
321
|
+
line += ` (${r.mimeType})`;
|
|
322
|
+
if (r.description)
|
|
323
|
+
line += `: ${r.description}`;
|
|
324
|
+
lines.push(line);
|
|
325
|
+
items.push({ server: name, uri: r.uri, name: r.name ?? "", mimeType: r.mimeType ?? "", description: r.description ?? "" });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch (e) {
|
|
329
|
+
lines.push(`${name}: error: ${e instanceof Error ? e.message : String(e)}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (!lines.length)
|
|
333
|
+
return { output: "No resources.", isError: false, metadata: { count: 0 } };
|
|
334
|
+
return { output: lines.join("\n"), isError: false, metadata: { count: items.length, resources: items } };
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
const read = {
|
|
338
|
+
name: "read_mcp_resource",
|
|
339
|
+
description: "Read one MCP resource by server and uri. Text comes back as text; binary content is described, not inlined.",
|
|
340
|
+
inputSchema: {
|
|
341
|
+
type: "object",
|
|
342
|
+
properties: {
|
|
343
|
+
server: { type: "string", description: "The MCP server name" },
|
|
344
|
+
uri: { type: "string", description: "The resource uri" },
|
|
345
|
+
},
|
|
346
|
+
required: ["server", "uri"],
|
|
347
|
+
additionalProperties: false,
|
|
348
|
+
},
|
|
349
|
+
source: "mcp",
|
|
350
|
+
hints: ro,
|
|
351
|
+
async execute(args, ctx) {
|
|
352
|
+
const server = typeof args?.server === "string" ? args.server : "";
|
|
353
|
+
const uri = typeof args?.uri === "string" ? args.uri : "";
|
|
354
|
+
let c;
|
|
355
|
+
try {
|
|
356
|
+
c = conn(server);
|
|
357
|
+
}
|
|
358
|
+
catch (e) {
|
|
359
|
+
return { output: `read_mcp_resource: ${e.message}`, isError: true };
|
|
360
|
+
}
|
|
361
|
+
if (!uri)
|
|
362
|
+
return { output: "read_mcp_resource: uri is required", isError: true };
|
|
363
|
+
try {
|
|
364
|
+
const res = await c.client.readResource({ uri }, { timeout: c.timeout, signal: ctx?.signal });
|
|
365
|
+
const parts = [];
|
|
366
|
+
for (const rc of res.contents ?? []) {
|
|
367
|
+
if (typeof rc.text === "string")
|
|
368
|
+
parts.push(rc.text);
|
|
369
|
+
else if (typeof rc.blob === "string")
|
|
370
|
+
parts.push(`[binary ${rc.mimeType ?? ""}, ${rc.blob.length} base64 chars]`);
|
|
371
|
+
}
|
|
372
|
+
return { output: parts.join("\n"), isError: false, metadata: { server, uri } };
|
|
373
|
+
}
|
|
374
|
+
catch (e) {
|
|
375
|
+
return { output: `read_mcp_resource: ${e instanceof Error ? e.message : String(e)}`, isError: true };
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
return [list, read];
|
|
380
|
+
}
|
|
238
381
|
/** Create a fresh (not-yet-connected) client, with the elicitation bridge attached if a waitFor
|
|
239
382
|
* exists. Separated from connect so the caller ALWAYS holds a closeable reference — even if the
|
|
240
383
|
* connect is later abandoned by a timeout/abort, `client.close()` still terminates any spawned
|
|
@@ -279,11 +422,28 @@ export async function loadMcp(input, opts) {
|
|
|
279
422
|
const tools = [];
|
|
280
423
|
const status = {};
|
|
281
424
|
const clients = [];
|
|
425
|
+
const servers = new Map();
|
|
282
426
|
const waitFor = opts?.waitFor;
|
|
283
427
|
const signal = opts?.signal;
|
|
284
|
-
|
|
428
|
+
const onServer = opts?.onServer;
|
|
429
|
+
let reports = Promise.resolve();
|
|
430
|
+
let reportError;
|
|
431
|
+
const report = (r) => {
|
|
432
|
+
if (!onServer)
|
|
433
|
+
return;
|
|
434
|
+
reports = reports.then(async () => {
|
|
435
|
+
try {
|
|
436
|
+
await onServer(r);
|
|
437
|
+
}
|
|
438
|
+
catch (e) {
|
|
439
|
+
reportError ??= e;
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
};
|
|
443
|
+
const loading = Promise.all(Object.entries(config).map(async ([name, cfg]) => {
|
|
285
444
|
if (!isEnabled(cfg)) {
|
|
286
445
|
status[name] = "disabled";
|
|
446
|
+
report({ name, status: "disabled", tools: [] });
|
|
287
447
|
return;
|
|
288
448
|
}
|
|
289
449
|
const timeout = cfg.timeout ?? DEFAULT_TIMEOUT;
|
|
@@ -294,10 +454,12 @@ export async function loadMcp(input, opts) {
|
|
|
294
454
|
await raceTimeout(connectTransport(client, cfg), timeout, signal);
|
|
295
455
|
const defs = await raceTimeout(paginateTools(client, timeout), timeout, signal);
|
|
296
456
|
const kept = applyToolsFilter(name, defs, cfg.tools);
|
|
297
|
-
|
|
298
|
-
|
|
457
|
+
const converted = kept.map((def) => convertTool(name, def, client, timeout));
|
|
458
|
+
tools.push(...converted);
|
|
299
459
|
clients.push(client);
|
|
460
|
+
servers.set(name, { client, timeout });
|
|
300
461
|
status[name] = "connected";
|
|
462
|
+
report({ name, status: "connected", tools: converted });
|
|
301
463
|
}
|
|
302
464
|
catch (e) {
|
|
303
465
|
status[name] = "failed";
|
|
@@ -306,15 +468,31 @@ export async function loadMcp(input, opts) {
|
|
|
306
468
|
// within budget isolates just this server.
|
|
307
469
|
if (signal?.aborted)
|
|
308
470
|
throw e;
|
|
471
|
+
report({ name, status: "failed", tools: [], error: e instanceof Error ? e : new Error(String(e)) });
|
|
309
472
|
console.warn(`[toolnexus] MCP server "${name}" failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
310
473
|
}
|
|
311
474
|
}));
|
|
475
|
+
try {
|
|
476
|
+
await loading;
|
|
477
|
+
}
|
|
478
|
+
catch (e) {
|
|
479
|
+
// Parent cancellation: close every client, including ones whose tools onServer handed out.
|
|
480
|
+
await reports;
|
|
481
|
+
await Promise.all(clients.map((c) => c.close().catch(() => { })));
|
|
482
|
+
throw e;
|
|
483
|
+
}
|
|
484
|
+
await reports;
|
|
485
|
+
if (reportError) {
|
|
486
|
+
await Promise.all(clients.map((c) => c.close().catch(() => { })));
|
|
487
|
+
throw reportError;
|
|
488
|
+
}
|
|
312
489
|
return {
|
|
313
490
|
tools,
|
|
314
491
|
status,
|
|
315
492
|
async close() {
|
|
316
493
|
await Promise.all(clients.map((c) => c.close().catch(() => { })));
|
|
317
494
|
},
|
|
495
|
+
resourceTools: () => resourceTools(servers),
|
|
318
496
|
};
|
|
319
497
|
}
|
|
320
498
|
/**
|
package/dist/models.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* listModels (SPEC §4C): the models a provider offers, for model switching. One GET per page,
|
|
3
|
+
* no retries. Same key rules as the client (apiKey, apiKeyEnv, or the host's own env var).
|
|
4
|
+
*/
|
|
5
|
+
import { type ClientOptions } from "./client.js";
|
|
6
|
+
/** One model a provider lists. */
|
|
7
|
+
export interface ModelInfo {
|
|
8
|
+
id: string;
|
|
9
|
+
/** The provider's display name, when it gives one. */
|
|
10
|
+
name?: string;
|
|
11
|
+
/** Context window in tokens when the provider reports it (OpenRouter does). */
|
|
12
|
+
contextLength?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* List the models at `opts.baseUrl` in `opts.style`: openai `GET {base}/models`; anthropic
|
|
16
|
+
* `GET {base}/v1/models`, following `has_more`/`last_id`. Sorted by id.
|
|
17
|
+
*/
|
|
18
|
+
export declare function listModels(opts: Pick<ClientOptions, "baseUrl" | "style" | "apiKey" | "apiKeyEnv" | "headers" | "fetch">, ctx?: {
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
}): Promise<ModelInfo[]>;
|