@tuppence/agent-tools 0.0.0-stage → 0.1.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/README.md CHANGED
@@ -1,3 +1,65 @@
1
- # Temporary Holding Version
1
+ # @tuppence/agent-tools
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Tuppence's tools for agent frameworks (the **OpenAI Agents SDK**, **LangChain** and the **Claude
4
+ Agent SDK**), straight from Tuppence's MCP server.
5
+
6
+ There is one definition of what an agent may do with Tuppence: the MCP server's. This package
7
+ asks it for its tools, with your key, and hands your framework the same tools in its own shape,
8
+ so they never drift, and a restricted key's agent is offered only what the key may do.
9
+
10
+ ```bash
11
+ npm install @tuppence/agent-tools
12
+ ```
13
+
14
+ Use a **restricted key** (`rk_…`) with only the scopes the agent needs, in `TUPPENCE_SECRET_KEY`
15
+ (or pass `apiKey`).
16
+
17
+ ## OpenAI Agents SDK
18
+
19
+ ```ts
20
+ import { Agent, run } from "@openai/agents";
21
+ import { tuppenceTools } from "@tuppence/agent-tools/openai-agents";
22
+
23
+ const { tools, close } = await tuppenceTools();
24
+ const agent = new Agent({ name: "Billing", instructions: "Amounts are in pence.", tools });
25
+ const result = await run(agent, "Make a £15 payment link for a research report");
26
+ await close();
27
+ ```
28
+
29
+ ## LangChain
30
+
31
+ ```ts
32
+ import { createAgent } from "langchain";
33
+ import { tuppenceTools } from "@tuppence/agent-tools/langchain";
34
+
35
+ const { tools, close } = await tuppenceTools();
36
+ const agent = createAgent({ model: "openai:gpt-5", tools });
37
+ await agent.invoke({ messages: [{ role: "user", content: "Make a £15 payment link" }] });
38
+ await close();
39
+ ```
40
+
41
+ ## Claude Agent SDK
42
+
43
+ It speaks MCP itself, so the pack is the connection:
44
+
45
+ ```ts
46
+ import { query } from "@anthropic-ai/claude-agent-sdk";
47
+ import { TUPPENCE_TOOLS, tuppenceMcpServer } from "@tuppence/agent-tools/claude-agent-sdk";
48
+
49
+ for await (const message of query({
50
+ prompt: "Make a £15 payment link for a research report",
51
+ options: { mcpServers: { tuppence: tuppenceMcpServer() }, allowedTools: [TUPPENCE_TOOLS] },
52
+ })) {
53
+ console.log(message);
54
+ }
55
+ ```
56
+
57
+ ## Refusals are answers
58
+
59
+ A tool the API refuses returns the error envelope (`code`, `message`, `param`) as its output,
60
+ not an exception, so the agent can read "`amount` must be between 30 and 99999999" and try
61
+ again. Money-moving tools need an `idempotency_key`: the same value when retrying, and the
62
+ payment happens once.
63
+
64
+ The examples in `examples/` run in Tuppence's CI against the real API, with a scripted model in
65
+ place of the LLM.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Tuppence for the Claude Agent SDK — which speaks MCP itself, so the pack is the connection:
3
+ *
4
+ * import { query } from "@anthropic-ai/claude-agent-sdk";
5
+ * import { TUPPENCE_TOOLS, tuppenceMcpServer } from "@tuppence/agent-tools/claude-agent-sdk";
6
+ *
7
+ * for await (const message of query({
8
+ * prompt: "Make a £15 payment link for a research report",
9
+ * options: {
10
+ * mcpServers: { tuppence: tuppenceMcpServer() }, // TUPPENCE_SECRET_KEY
11
+ * allowedTools: [TUPPENCE_TOOLS], // or name them: mcp__tuppence__create_payment_link
12
+ * },
13
+ * })) { … }
14
+ */
15
+ import { type ConnectOptions } from "./index.js";
16
+ /** Every Tuppence tool, as the Claude Agent SDK names them (`mcp__<server>__<tool>`). */
17
+ export declare const TUPPENCE_TOOLS = "mcp__tuppence";
18
+ /**
19
+ * The remote MCP server, as an `mcpServers` entry: streamable HTTP, your key as a bearer — and,
20
+ * with `account`, as one of your connected accounts (Tuppence Connect).
21
+ */
22
+ export declare function tuppenceMcpServer(options?: ConnectOptions): {
23
+ type: "http";
24
+ url: string;
25
+ headers: Record<string, string>;
26
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Tuppence for the Claude Agent SDK — which speaks MCP itself, so the pack is the connection:
3
+ *
4
+ * import { query } from "@anthropic-ai/claude-agent-sdk";
5
+ * import { TUPPENCE_TOOLS, tuppenceMcpServer } from "@tuppence/agent-tools/claude-agent-sdk";
6
+ *
7
+ * for await (const message of query({
8
+ * prompt: "Make a £15 payment link for a research report",
9
+ * options: {
10
+ * mcpServers: { tuppence: tuppenceMcpServer() }, // TUPPENCE_SECRET_KEY
11
+ * allowedTools: [TUPPENCE_TOOLS], // or name them: mcp__tuppence__create_payment_link
12
+ * },
13
+ * })) { … }
14
+ */
15
+ import { resolveOptions } from "./index.js";
16
+ /** Every Tuppence tool, as the Claude Agent SDK names them (`mcp__<server>__<tool>`). */
17
+ export const TUPPENCE_TOOLS = "mcp__tuppence";
18
+ /**
19
+ * The remote MCP server, as an `mcpServers` entry: streamable HTTP, your key as a bearer — and,
20
+ * with `account`, as one of your connected accounts (Tuppence Connect).
21
+ */
22
+ export function tuppenceMcpServer(options = {}) {
23
+ const { apiUrl, headers } = resolveOptions(options);
24
+ return { type: "http", url: `${apiUrl}/mcp`, headers };
25
+ }
@@ -0,0 +1,49 @@
1
+ export declare const VERSION = "0.1.1";
2
+ export type ConnectOptions = {
3
+ /** Default: `TUPPENCE_SECRET_KEY` (or `TUPPENCE_API_KEY`). Prefer a restricted key (`rk_…`). */
4
+ apiKey?: string;
5
+ /** Default: `TUPPENCE_API_URL`, else `https://api.tuppence.ai`. */
6
+ apiUrl?: string;
7
+ /**
8
+ * Tuppence Connect: every tool acts as this one of your connected accounts (`acct_…`) — the
9
+ * `Tuppence-Account` header on the connection, as `tuppence.as()` is in `@tuppence/node`. The
10
+ * platform's own Connect tools are then not offered. Without it, a tool call's own `account`
11
+ * argument acts as one account for that call.
12
+ */
13
+ account?: string;
14
+ };
15
+ export type ToolSpec = {
16
+ name: string;
17
+ title?: string;
18
+ description: string;
19
+ /** JSON Schema for the arguments — exactly what the MCP server declares. */
20
+ inputSchema: Record<string, unknown>;
21
+ annotations?: {
22
+ readOnlyHint?: boolean;
23
+ destructiveHint?: boolean;
24
+ idempotentHint?: boolean;
25
+ };
26
+ };
27
+ /** What a tool call returned: the API's JSON — on failure, its error envelope, with `isError`. */
28
+ export type ToolResult = {
29
+ isError: boolean;
30
+ body: unknown;
31
+ };
32
+ export declare function resolveOptions(options?: ConnectOptions): {
33
+ apiKey: string;
34
+ apiUrl: string;
35
+ headers: Record<string, string>;
36
+ };
37
+ export declare class TuppenceToolkit {
38
+ private readonly client;
39
+ /** The tools this key may use, as the server describes them. */
40
+ readonly tools: ToolSpec[];
41
+ private constructor();
42
+ static connect(options?: ConnectOptions): Promise<TuppenceToolkit>;
43
+ /**
44
+ * Calls a tool. A refusal is an ANSWER, not an exception: the agent gets the error envelope
45
+ * (`code`, `message`, `param`) as the tool's output, and can correct itself and try again.
46
+ */
47
+ call(name: string, args: Record<string, unknown>): Promise<ToolResult>;
48
+ close(): Promise<void>;
49
+ }
package/dist/index.js ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Tuppence's tools, for agent frameworks (plan 11-10).
3
+ *
4
+ * There is ONE definition of what an agent may do with Tuppence: the MCP server's (the API's
5
+ * `mcp.tools.ts` — the same services as the REST API, the same scopes, the same idempotency).
6
+ * This package does not copy it: it asks the server for its tools (`tools/list`), with your key,
7
+ * and hands each framework the same tools in its own shape. So the packs cannot drift from the
8
+ * server, or from each other — and a restricted key's agent is offered only what the key may do.
9
+ *
10
+ * import { tuppenceTools } from "@tuppence/agent-tools/openai-agents"; // OpenAI Agents SDK
11
+ * import { tuppenceTools } from "@tuppence/agent-tools/langchain"; // LangChain
12
+ * import { tuppenceMcpServer } from "@tuppence/agent-tools/claude-agent-sdk";
13
+ */
14
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
15
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
16
+ export const VERSION = "0.1.1";
17
+ export function resolveOptions(options = {}) {
18
+ const env = globalThis.process?.env ?? {};
19
+ const apiKey = options.apiKey ?? env.TUPPENCE_SECRET_KEY ?? env.TUPPENCE_API_KEY ?? "";
20
+ if (!/^(sk|rk)_(test|live)_/.test(apiKey)) {
21
+ throw new TypeError("A Tuppence secret or restricted key is needed (sk_… / rk_…): pass apiKey, or set TUPPENCE_SECRET_KEY.");
22
+ }
23
+ const apiUrl = (options.apiUrl ?? env.TUPPENCE_API_URL ?? "https://api.tuppence.ai").replace(/\/+$/, "");
24
+ const account = options.account?.trim();
25
+ if (account !== undefined && !/^acct_\w+$/.test(account)) {
26
+ throw new TypeError('`account` is a connected account id: "acct_…".');
27
+ }
28
+ return {
29
+ apiKey,
30
+ apiUrl,
31
+ headers: {
32
+ Authorization: `Bearer ${apiKey}`,
33
+ ...(account ? { "Tuppence-Account": account } : {}),
34
+ },
35
+ };
36
+ }
37
+ export class TuppenceToolkit {
38
+ client;
39
+ tools;
40
+ constructor(client,
41
+ /** The tools this key may use, as the server describes them. */
42
+ tools) {
43
+ this.client = client;
44
+ this.tools = tools;
45
+ }
46
+ static async connect(options = {}) {
47
+ const { apiUrl, headers } = resolveOptions(options);
48
+ const client = new Client({ name: "tuppence-agent-tools", version: VERSION });
49
+ await client.connect(new StreamableHTTPClientTransport(new URL(`${apiUrl}/mcp`), { requestInit: { headers } }));
50
+ const { tools } = await client.listTools();
51
+ return new TuppenceToolkit(client, tools.map((t) => ({
52
+ name: t.name,
53
+ ...(t.title ? { title: t.title } : {}),
54
+ description: t.description ?? t.title ?? t.name,
55
+ inputSchema: t.inputSchema,
56
+ ...(t.annotations ? { annotations: t.annotations } : {}),
57
+ })));
58
+ }
59
+ /**
60
+ * Calls a tool. A refusal is an ANSWER, not an exception: the agent gets the error envelope
61
+ * (`code`, `message`, `param`) as the tool's output, and can correct itself and try again.
62
+ */
63
+ async call(name, args) {
64
+ const result = await this.client.callTool({ name, arguments: args });
65
+ const text = result.content?.find((c) => c.type === "text")?.text ?? "null";
66
+ let body = text;
67
+ try {
68
+ body = JSON.parse(text);
69
+ }
70
+ catch {
71
+ // Not JSON: hand the text over as it is.
72
+ }
73
+ return { isError: result.isError === true, body };
74
+ }
75
+ close() {
76
+ return this.client.close();
77
+ }
78
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Tuppence's tools for LangChain (JS).
3
+ *
4
+ * import { createAgent } from "langchain";
5
+ * import { tuppenceTools } from "@tuppence/agent-tools/langchain";
6
+ *
7
+ * const { tools, close } = await tuppenceTools(); // TUPPENCE_SECRET_KEY
8
+ * const agent = createAgent({ model: "openai:gpt-5", tools });
9
+ * await agent.invoke({ messages: [{ role: "user", content: "Make a £15 link" }] });
10
+ * await close();
11
+ */
12
+ import { type StructuredToolInterface } from "@langchain/core/tools";
13
+ import { type ConnectOptions, TuppenceToolkit } from "./index.js";
14
+ export declare function tuppenceTools(options?: ConnectOptions): Promise<{
15
+ tools: StructuredToolInterface[];
16
+ toolkit: TuppenceToolkit;
17
+ close: () => Promise<void>;
18
+ }>;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Tuppence's tools for LangChain (JS).
3
+ *
4
+ * import { createAgent } from "langchain";
5
+ * import { tuppenceTools } from "@tuppence/agent-tools/langchain";
6
+ *
7
+ * const { tools, close } = await tuppenceTools(); // TUPPENCE_SECRET_KEY
8
+ * const agent = createAgent({ model: "openai:gpt-5", tools });
9
+ * await agent.invoke({ messages: [{ role: "user", content: "Make a £15 link" }] });
10
+ * await close();
11
+ */
12
+ import { tool } from "@langchain/core/tools";
13
+ import { TuppenceToolkit } from "./index.js";
14
+ export async function tuppenceTools(options = {}) {
15
+ const toolkit = await TuppenceToolkit.connect(options);
16
+ const tools = toolkit.tools.map((spec) => tool(async (args) => JSON.stringify((await toolkit.call(spec.name, args)).body), {
17
+ name: spec.name,
18
+ description: spec.description,
19
+ // LangChain takes the server's JSON Schema as is.
20
+ schema: spec.inputSchema,
21
+ }));
22
+ return { tools, toolkit, close: () => toolkit.close() };
23
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Tuppence's tools for the OpenAI Agents SDK.
3
+ *
4
+ * import { Agent, run } from "@openai/agents";
5
+ * import { tuppenceTools } from "@tuppence/agent-tools/openai-agents";
6
+ *
7
+ * const { tools, close } = await tuppenceTools(); // TUPPENCE_SECRET_KEY
8
+ * const agent = new Agent({ name: "Billing", instructions: "…", tools });
9
+ * const result = await run(agent, "Make a £15 link for a research report");
10
+ * await close();
11
+ */
12
+ import { type FunctionTool } from "@openai/agents";
13
+ import { type ConnectOptions, TuppenceToolkit } from "./index.js";
14
+ export declare function tuppenceTools(options?: ConnectOptions): Promise<{
15
+ tools: FunctionTool[];
16
+ toolkit: TuppenceToolkit;
17
+ close: () => Promise<void>;
18
+ }>;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Tuppence's tools for the OpenAI Agents SDK.
3
+ *
4
+ * import { Agent, run } from "@openai/agents";
5
+ * import { tuppenceTools } from "@tuppence/agent-tools/openai-agents";
6
+ *
7
+ * const { tools, close } = await tuppenceTools(); // TUPPENCE_SECRET_KEY
8
+ * const agent = new Agent({ name: "Billing", instructions: "…", tools });
9
+ * const result = await run(agent, "Make a £15 link for a research report");
10
+ * await close();
11
+ */
12
+ import { tool } from "@openai/agents";
13
+ import { TuppenceToolkit } from "./index.js";
14
+ export async function tuppenceTools(options = {}) {
15
+ const toolkit = await TuppenceToolkit.connect(options);
16
+ const tools = toolkit.tools.map((spec) => tool({
17
+ name: spec.name,
18
+ description: spec.description,
19
+ // The server's JSON Schema, as is. Not "strict": some arguments are optional.
20
+ parameters: {
21
+ type: "object",
22
+ properties: {},
23
+ required: [],
24
+ additionalProperties: true,
25
+ ...spec.inputSchema,
26
+ },
27
+ strict: false,
28
+ execute: async (args) => JSON.stringify((await toolkit.call(spec.name, args)).body),
29
+ }));
30
+ return { tools, toolkit, close: () => toolkit.close() };
31
+ }
package/package.json CHANGED
@@ -1,6 +1,69 @@
1
1
  {
2
2
  "name": "@tuppence/agent-tools",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "Tuppence's tools for agent frameworks — the OpenAI Agents SDK, LangChain and the Claude Agent SDK — straight from its MCP server.",
5
+ "license": "MIT",
6
+ "homepage": "https://docs.tuppence.ai",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ZenningAI/tuppence.git",
10
+ "directory": "packages/agent-tools"
11
+ },
12
+ "bugs": "https://github.com/ZenningAI/tuppence/issues",
13
+ "type": "module",
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ },
22
+ "./openai-agents": {
23
+ "types": "./dist/openai-agents.d.ts",
24
+ "import": "./dist/openai-agents.js"
25
+ },
26
+ "./langchain": {
27
+ "types": "./dist/langchain.d.ts",
28
+ "import": "./dist/langchain.js"
29
+ },
30
+ "./claude-agent-sdk": {
31
+ "types": "./dist/claude-agent-sdk.d.ts",
32
+ "import": "./dist/claude-agent-sdk.js"
33
+ }
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "README.md"
38
+ ],
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "1.32.1"
41
+ },
42
+ "peerDependencies": {
43
+ "@langchain/core": ">=1.2.0",
44
+ "@openai/agents": ">=0.18.0"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "@langchain/core": {
48
+ "optional": true
49
+ },
50
+ "@openai/agents": {
51
+ "optional": true
52
+ }
53
+ },
54
+ "devDependencies": {
55
+ "@langchain/core": "1.2.12",
56
+ "@openai/agents": "0.18.0",
57
+ "@types/node": "^22.20.4",
58
+ "langchain": "1.5.11",
59
+ "typescript": "^5.9.3",
60
+ "zod": "^4.6.6"
61
+ },
62
+ "publishConfig": {
63
+ "access": "public"
64
+ },
65
+ "scripts": {
66
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
67
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.examples.json --noEmit"
68
+ }
6
69
  }