toolnexus 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/dist/adapters.d.ts +26 -0
- package/dist/adapters.js +24 -0
- package/dist/client.d.ts +169 -0
- package/dist/client.js +447 -0
- package/dist/http.d.ts +21 -0
- package/dist/http.js +90 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/mcp.d.ts +35 -0
- package/dist/mcp.js +169 -0
- package/dist/native.d.ts +21 -0
- package/dist/native.js +57 -0
- package/dist/skill.d.ts +16 -0
- package/dist/skill.js +184 -0
- package/dist/toolkit.d.ts +47 -0
- package/dist/toolkit.js +71 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.js +5 -0
- package/package.json +29 -0
- package/src/adapters.ts +33 -0
- package/src/client.ts +495 -0
- package/src/http.ts +110 -0
- package/src/index.ts +8 -0
- package/src/mcp.ts +210 -0
- package/src/native.ts +72 -0
- package/src/skill.ts +195 -0
- package/src/toolkit.ts +93 -0
- package/src/types.ts +37 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Muthukumaran Navaneethakrishnan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# toolnexus (JavaScript / TypeScript)
|
|
2
|
+
|
|
3
|
+
Dynamic MCP servers + agent skills as a uniform tool list for any LLM. Built on
|
|
4
|
+
`@modelcontextprotocol/sdk` (the same MCP SDK opencode uses). See the root
|
|
5
|
+
[SPEC.md](../SPEC.md) for the language-independent contract.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install toolnexus
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Use
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createToolkit } from "toolnexus"
|
|
17
|
+
|
|
18
|
+
const tk = await createToolkit({
|
|
19
|
+
mcpConfig: "./mcp.json", // path or parsed object; key may be mcpServers/servers/mcp
|
|
20
|
+
skillsDir: "./skills", // folder of <name>/SKILL.md (or an array of folders)
|
|
21
|
+
// extraTools: [...] // your own custom tools, optional
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// 1. give the model the tool schema in its native format
|
|
25
|
+
const tools = tk.toOpenAI() // or tk.toAnthropic() / tk.toGemini()
|
|
26
|
+
const system = tk.skillsPrompt() // markdown catalog of skills for the system prompt
|
|
27
|
+
|
|
28
|
+
// 2. when the model returns a tool call { name, arguments }:
|
|
29
|
+
const res = await tk.execute(name, args) // -> { output, isError, metadata }
|
|
30
|
+
// feed res.output back as the tool result, loop.
|
|
31
|
+
|
|
32
|
+
console.log(tk.mcpStatus()) // { server: "connected" | "failed" | "disabled" }
|
|
33
|
+
await tk.close()
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Run the examples
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
npm install && npm run build
|
|
40
|
+
|
|
41
|
+
# offline: connects to @modelcontextprotocol/server-everything via npx, lists tools, loads a skill
|
|
42
|
+
node --experimental-strip-types examples/basic.ts
|
|
43
|
+
|
|
44
|
+
# live: real OpenRouter tool-calling round trip (needs OPENROUTER_API_KEY)
|
|
45
|
+
OPENROUTER_API_KEY=... node --experimental-strip-types examples/openrouter_test.ts
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## API
|
|
49
|
+
|
|
50
|
+
| Member | Description |
|
|
51
|
+
|--------|-------------|
|
|
52
|
+
| `createToolkit(opts)` | async factory → `Toolkit` |
|
|
53
|
+
| `tk.tools()` | all uniform `Tool`s (mcp + skill + extras) |
|
|
54
|
+
| `tk.get(name)` | one tool by name |
|
|
55
|
+
| `tk.execute(name, args, ctx?)` | run a tool → `ToolResult` |
|
|
56
|
+
| `tk.skillsPrompt()` | system-prompt skill catalog |
|
|
57
|
+
| `tk.mcpStatus()` | per-server connection status |
|
|
58
|
+
| `tk.toOpenAI()` / `toAnthropic()` / `toGemini()` | provider tool schemas |
|
|
59
|
+
| `tk.register(...tools)` | add native/http/custom tools at runtime |
|
|
60
|
+
| `tk.close()` | disconnect MCP servers |
|
|
61
|
+
|
|
62
|
+
You can also use the sources directly: `loadMcp(config)`, `loadSkills(dir)`.
|
|
63
|
+
|
|
64
|
+
## Native tools (a function → a tool)
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { defineTool } from "toolnexus"
|
|
68
|
+
|
|
69
|
+
const add = defineTool({
|
|
70
|
+
name: "add", description: "Add two numbers",
|
|
71
|
+
inputSchema: { type: "object", properties: { a: { type: "number" }, b: { type: "number" } }, required: ["a", "b"] },
|
|
72
|
+
run: ({ a, b }) => `${(a as number) + (b as number)}`, // string or ToolResult
|
|
73
|
+
})
|
|
74
|
+
tk.register(add)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## HTTP / REST tools (an endpoint → a tool)
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { httpTool } from "toolnexus"
|
|
81
|
+
|
|
82
|
+
tk.register(httpTool({
|
|
83
|
+
name: "create_ticket", description: "Create a ticket", method: "POST",
|
|
84
|
+
url: "https://api.example.com/tickets",
|
|
85
|
+
headers: { Authorization: "Bearer ${API_TOKEN}" }, // ${ENV} expands from process.env, never logged
|
|
86
|
+
inputSchema: { type: "object", properties: { title: { type: "string" } }, required: ["title"] },
|
|
87
|
+
}))
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
URL `{placeholders}` are filled from args; `query` args go to the querystring; the rest
|
|
91
|
+
become the JSON body. Non-2xx → `{ isError: true, output: "HTTP <status>: <body>" }`.
|
|
92
|
+
|
|
93
|
+
## Unified client (the host loop)
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { createClient } from "toolnexus"
|
|
97
|
+
|
|
98
|
+
const agent = createClient({
|
|
99
|
+
baseUrl: "https://openrouter.ai/api/v1", // any OpenAI- or Anthropic-style endpoint
|
|
100
|
+
style: "openai", // or "anthropic"
|
|
101
|
+
model: "openai/gpt-4o-mini",
|
|
102
|
+
// apiKey defaults to OPENROUTER_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY
|
|
103
|
+
systemPrompt: "You are a helpful agent.",
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const { text, toolCalls } = await agent.run("do the thing", { toolkit: tk })
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
It injects `tk.skillsPrompt()` into the system message and runs the full
|
|
110
|
+
call → execute tools → feed back → repeat loop (up to `maxTurns`, default 10).
|
|
111
|
+
|
|
112
|
+
## Examples
|
|
113
|
+
|
|
114
|
+
- `examples/basic.ts` — toolkit + adapters + skill load (offline).
|
|
115
|
+
- `examples/agent.ts` — native + http tools driven by the unified client (live with `OPENROUTER_API_KEY`).
|
|
116
|
+
- `examples/openrouter_test.ts` — minimal live tool-calling round trip.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider adapters — turn the uniform tool list into each LLM's tool schema.
|
|
3
|
+
* Execution is identical for every provider: read the tool name + args the model
|
|
4
|
+
* returned, call toolkit.execute(name, args), feed output back.
|
|
5
|
+
*/
|
|
6
|
+
import type { Tool } from "./types.js";
|
|
7
|
+
export declare function toOpenAI(tools: Tool[]): {
|
|
8
|
+
type: "function";
|
|
9
|
+
function: {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
parameters: import("./types.js").JSONSchema;
|
|
13
|
+
};
|
|
14
|
+
}[];
|
|
15
|
+
export declare function toAnthropic(tools: Tool[]): {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
input_schema: import("./types.js").JSONSchema;
|
|
19
|
+
}[];
|
|
20
|
+
export declare function toGemini(tools: Tool[]): {
|
|
21
|
+
functionDeclarations: {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
parameters: import("./types.js").JSONSchema;
|
|
25
|
+
}[];
|
|
26
|
+
}[];
|
package/dist/adapters.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function toOpenAI(tools) {
|
|
2
|
+
return tools.map((t) => ({
|
|
3
|
+
type: "function",
|
|
4
|
+
function: { name: t.name, description: t.description, parameters: t.inputSchema },
|
|
5
|
+
}));
|
|
6
|
+
}
|
|
7
|
+
export function toAnthropic(tools) {
|
|
8
|
+
return tools.map((t) => ({
|
|
9
|
+
name: t.name,
|
|
10
|
+
description: t.description,
|
|
11
|
+
input_schema: t.inputSchema,
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
export function toGemini(tools) {
|
|
15
|
+
return [
|
|
16
|
+
{
|
|
17
|
+
functionDeclarations: tools.map((t) => ({
|
|
18
|
+
name: t.name,
|
|
19
|
+
description: t.description,
|
|
20
|
+
parameters: t.inputSchema,
|
|
21
|
+
})),
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified LLM client — the host loop. Give it a plain base URL + a style
|
|
3
|
+
* ("openai" | "anthropic"), and it runs the tool-calling agent loop against a
|
|
4
|
+
* Toolkit. See ../../SPEC.md §8.
|
|
5
|
+
*/
|
|
6
|
+
import type { Toolkit } from "./toolkit.js";
|
|
7
|
+
import type { ToolResult } from "./types.js";
|
|
8
|
+
export type ClientStyle = "openai" | "anthropic";
|
|
9
|
+
export interface ClientOptions {
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
style: ClientStyle;
|
|
12
|
+
model: string;
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
headers?: Record<string, string>;
|
|
15
|
+
systemPrompt?: string;
|
|
16
|
+
maxTurns?: number;
|
|
17
|
+
hooks?: Hooks;
|
|
18
|
+
/** Retries on transient LLM errors (429/5xx/network). Default 2. */
|
|
19
|
+
retries?: number;
|
|
20
|
+
/** Base backoff in ms (exponential + jitter). Default 500. */
|
|
21
|
+
retryBaseMs?: number;
|
|
22
|
+
/** Whole-run deadline in ms; aborts the run (and its in-flight request) when exceeded. */
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Lifecycle hooks around the agent loop. Each may observe, and (where noted)
|
|
27
|
+
* mutate or short-circuit. All may be async.
|
|
28
|
+
*/
|
|
29
|
+
export interface Hooks {
|
|
30
|
+
/** Before each model call. Return { messages?, tools? } to replace them. */
|
|
31
|
+
beforeLLM?(ev: {
|
|
32
|
+
messages: any[];
|
|
33
|
+
tools: any[];
|
|
34
|
+
model: string;
|
|
35
|
+
turn: number;
|
|
36
|
+
}): void | {
|
|
37
|
+
messages?: any[];
|
|
38
|
+
tools?: any[];
|
|
39
|
+
} | Promise<void | {
|
|
40
|
+
messages?: any[];
|
|
41
|
+
tools?: any[];
|
|
42
|
+
}>;
|
|
43
|
+
/** After each model call (observe: logging, cost, tracing). */
|
|
44
|
+
afterLLM?(ev: {
|
|
45
|
+
response: any;
|
|
46
|
+
model: string;
|
|
47
|
+
turn: number;
|
|
48
|
+
}): void | Promise<void>;
|
|
49
|
+
/** Before a tool runs. Return { result } to SHORT-CIRCUIT (deny/cache), { args } to rewrite. */
|
|
50
|
+
beforeTool?(ev: {
|
|
51
|
+
name: string;
|
|
52
|
+
args: Record<string, unknown>;
|
|
53
|
+
id?: string;
|
|
54
|
+
turn: number;
|
|
55
|
+
}): void | {
|
|
56
|
+
args?: Record<string, unknown>;
|
|
57
|
+
result?: ToolResult;
|
|
58
|
+
} | Promise<void | {
|
|
59
|
+
args?: Record<string, unknown>;
|
|
60
|
+
result?: ToolResult;
|
|
61
|
+
}>;
|
|
62
|
+
/** After a tool runs. Return { result } to replace the result. */
|
|
63
|
+
afterTool?(ev: {
|
|
64
|
+
name: string;
|
|
65
|
+
args: Record<string, unknown>;
|
|
66
|
+
result: ToolResult;
|
|
67
|
+
id?: string;
|
|
68
|
+
turn: number;
|
|
69
|
+
}): void | {
|
|
70
|
+
result?: ToolResult;
|
|
71
|
+
} | Promise<void | {
|
|
72
|
+
result?: ToolResult;
|
|
73
|
+
}>;
|
|
74
|
+
}
|
|
75
|
+
/** A tool call the model made, plus its result + metadata. */
|
|
76
|
+
export interface ToolCallRecord {
|
|
77
|
+
name: string;
|
|
78
|
+
args: Record<string, unknown>;
|
|
79
|
+
output: string;
|
|
80
|
+
isError: boolean;
|
|
81
|
+
metadata?: Record<string, unknown>;
|
|
82
|
+
}
|
|
83
|
+
/** Token usage, summed across every LLM round trip in the run. */
|
|
84
|
+
export interface Usage {
|
|
85
|
+
promptTokens: number;
|
|
86
|
+
completionTokens: number;
|
|
87
|
+
totalTokens: number;
|
|
88
|
+
}
|
|
89
|
+
export interface RunResult {
|
|
90
|
+
text: string;
|
|
91
|
+
messages: any[];
|
|
92
|
+
/** Every tool call made, with its output, error flag, and metadata. */
|
|
93
|
+
toolCalls: ToolCallRecord[];
|
|
94
|
+
/** Total number of tool calls (= toolCalls.length). */
|
|
95
|
+
toolCallCount: number;
|
|
96
|
+
/** Number of LLM round trips. */
|
|
97
|
+
turns: number;
|
|
98
|
+
/** Aggregated token usage across all turns. */
|
|
99
|
+
usage: Usage;
|
|
100
|
+
/** The model used. */
|
|
101
|
+
model: string;
|
|
102
|
+
}
|
|
103
|
+
/** Events yielded by client.stream(). */
|
|
104
|
+
export type StreamEvent = {
|
|
105
|
+
type: "text";
|
|
106
|
+
delta: string;
|
|
107
|
+
} | {
|
|
108
|
+
type: "tool_call";
|
|
109
|
+
id: string;
|
|
110
|
+
name: string;
|
|
111
|
+
args: Record<string, unknown>;
|
|
112
|
+
} | {
|
|
113
|
+
type: "tool_result";
|
|
114
|
+
id: string;
|
|
115
|
+
name: string;
|
|
116
|
+
output: string;
|
|
117
|
+
isError: boolean;
|
|
118
|
+
} | {
|
|
119
|
+
type: "usage";
|
|
120
|
+
usage: Usage;
|
|
121
|
+
} | {
|
|
122
|
+
type: "done";
|
|
123
|
+
result: RunResult;
|
|
124
|
+
};
|
|
125
|
+
export declare class Client {
|
|
126
|
+
private readonly opts;
|
|
127
|
+
constructor(opts: ClientOptions);
|
|
128
|
+
run(prompt: string, ctx: {
|
|
129
|
+
toolkit: Toolkit;
|
|
130
|
+
signal?: AbortSignal;
|
|
131
|
+
history?: any[];
|
|
132
|
+
}): Promise<RunResult>;
|
|
133
|
+
/** A stateful multi-turn conversation that retains history across sends. */
|
|
134
|
+
conversation(ctx: {
|
|
135
|
+
toolkit: Toolkit;
|
|
136
|
+
signal?: AbortSignal;
|
|
137
|
+
}): Conversation;
|
|
138
|
+
/** Streaming variant: async-iterate live events (text deltas, tool calls/results, usage, done). */
|
|
139
|
+
stream(prompt: string, ctx: {
|
|
140
|
+
toolkit: Toolkit;
|
|
141
|
+
signal?: AbortSignal;
|
|
142
|
+
}): AsyncGenerator<StreamEvent, void, unknown>;
|
|
143
|
+
private system;
|
|
144
|
+
/** Build a run-scoped abort signal from the optional run-level timeout + an external signal. */
|
|
145
|
+
private makeSignal;
|
|
146
|
+
/** fetch with retry + exponential backoff on 429/5xx/network, honoring Retry-After; aborts via signal. */
|
|
147
|
+
private llmFetch;
|
|
148
|
+
/** Run one tool through beforeTool/afterTool hooks (mutate args, short-circuit, transform result). */
|
|
149
|
+
private runTool;
|
|
150
|
+
private runOpenAI;
|
|
151
|
+
private result;
|
|
152
|
+
private runAnthropic;
|
|
153
|
+
private streamOpenAI;
|
|
154
|
+
private streamAnthropic;
|
|
155
|
+
}
|
|
156
|
+
export declare function createClient(opts: ClientOptions): Client;
|
|
157
|
+
/** Stateful multi-turn conversation: each send() continues the same transcript (memory). */
|
|
158
|
+
export declare class Conversation {
|
|
159
|
+
private readonly client;
|
|
160
|
+
private readonly toolkit;
|
|
161
|
+
private readonly signal?;
|
|
162
|
+
/** Full running transcript (system + user + assistant + tool messages). */
|
|
163
|
+
messages: any[];
|
|
164
|
+
constructor(client: Client, toolkit: Toolkit, signal?: AbortSignal | undefined);
|
|
165
|
+
/** Send the next user turn; prior history is retained automatically. */
|
|
166
|
+
send(prompt: string): Promise<RunResult>;
|
|
167
|
+
/** Reset the conversation memory. */
|
|
168
|
+
reset(): void;
|
|
169
|
+
}
|