typed-bridge 2.1.4 → 3.0.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.
@@ -0,0 +1,126 @@
1
+ import { z } from 'zod';
2
+ export type BridgeEntry = {
3
+ handler: (...args: any[]) => Promise<any>;
4
+ description?: string;
5
+ args?: z.ZodType;
6
+ res: z.ZodType;
7
+ mcp?: boolean;
8
+ llm?: boolean;
9
+ };
10
+ export type BridgeEntries = Record<string, BridgeEntry>;
11
+ export type ExtractHandlers<T extends BridgeEntries> = {
12
+ [K in keyof T]: T[K] extends {
13
+ handler: infer H;
14
+ } ? H : never;
15
+ };
16
+ export type Bridge = Record<string, (...args: any[]) => Promise<any>>;
17
+ export declare const LLM_TOOL_FORMATS: readonly ["openai", "anthropic", "json-schema"];
18
+ export type LLMToolFormat = (typeof LLM_TOOL_FORMATS)[number];
19
+ export declare function isLLMToolFormat(value: string): value is LLMToolFormat;
20
+ export interface ToLLMToolsOptions {
21
+ format?: LLMToolFormat;
22
+ includeResponse?: boolean;
23
+ }
24
+ export interface ToolCall {
25
+ name: string;
26
+ arguments: Record<string, unknown>;
27
+ }
28
+ export declare function defineBridge<T extends BridgeEntries>(entries: T): ExtractHandlers<T>;
29
+ export declare function toToolInputSchema(args?: z.ZodType): any;
30
+ export declare function schemaToJSONSchema(schema: z.ZodType): any;
31
+ export declare function toLLMTools(entries: BridgeEntries, options?: ToLLMToolsOptions): unknown[];
32
+ export declare function getMetaTools(options?: {
33
+ format?: LLMToolFormat;
34
+ }): ({
35
+ name: string;
36
+ description: string;
37
+ parameters: {
38
+ type: string;
39
+ properties: {
40
+ query: {
41
+ type: string;
42
+ description: string;
43
+ };
44
+ };
45
+ };
46
+ } | {
47
+ name: string;
48
+ description: string;
49
+ parameters: {
50
+ type: string;
51
+ properties: {
52
+ name: {
53
+ type: string;
54
+ description: string;
55
+ };
56
+ };
57
+ required: string[];
58
+ };
59
+ })[] | {
60
+ type: string;
61
+ function: {
62
+ name: string;
63
+ description: string;
64
+ parameters: {
65
+ type: string;
66
+ properties: {
67
+ query: {
68
+ type: string;
69
+ description: string;
70
+ };
71
+ };
72
+ };
73
+ } | {
74
+ name: string;
75
+ description: string;
76
+ parameters: {
77
+ type: string;
78
+ properties: {
79
+ name: {
80
+ type: string;
81
+ description: string;
82
+ };
83
+ };
84
+ required: string[];
85
+ };
86
+ };
87
+ }[] | {
88
+ name: string;
89
+ description: string;
90
+ input_schema: {
91
+ type: string;
92
+ properties: {
93
+ query: {
94
+ type: string;
95
+ description: string;
96
+ };
97
+ };
98
+ } | {
99
+ type: string;
100
+ properties: {
101
+ name: {
102
+ type: string;
103
+ description: string;
104
+ };
105
+ };
106
+ required: string[];
107
+ };
108
+ }[];
109
+ export declare function toolSearch(entries: BridgeEntries, query?: string): {
110
+ name: string;
111
+ description?: string;
112
+ }[];
113
+ export declare function toolDescribe(entries: BridgeEntries, name: string): {
114
+ name: string;
115
+ description: string | undefined;
116
+ args: any;
117
+ response: any;
118
+ };
119
+ /**
120
+ * Serialize a tool result and enforce `tbConfig.maxToolOutputChars`. Oversized results
121
+ * throw (instead of truncating to invalid JSON) so the model is prompted to narrow its
122
+ * query. Returns the serialized JSON so callers can reuse it without re-stringifying.
123
+ */
124
+ export declare function enforceToolOutputLimit(result: unknown): string;
125
+ export declare function toolUse(bridge: Bridge, name: string, args: unknown, context?: unknown): Promise<any>;
126
+ export declare function handleMetaToolCall(bridge: Bridge, entries: BridgeEntries, toolCall: ToolCall, context?: unknown): Promise<unknown>;
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LLM_TOOL_FORMATS = void 0;
4
+ exports.isLLMToolFormat = isLLMToolFormat;
5
+ exports.defineBridge = defineBridge;
6
+ exports.toToolInputSchema = toToolInputSchema;
7
+ exports.schemaToJSONSchema = schemaToJSONSchema;
8
+ exports.toLLMTools = toLLMTools;
9
+ exports.getMetaTools = getMetaTools;
10
+ exports.toolSearch = toolSearch;
11
+ exports.toolDescribe = toolDescribe;
12
+ exports.enforceToolOutputLimit = enforceToolOutputLimit;
13
+ exports.toolUse = toolUse;
14
+ exports.handleMetaToolCall = handleMetaToolCall;
15
+ const zod_1 = require("zod");
16
+ const config_1 = require("../config");
17
+ exports.LLM_TOOL_FORMATS = ['openai', 'anthropic', 'json-schema'];
18
+ function isLLMToolFormat(value) {
19
+ return exports.LLM_TOOL_FORMATS.includes(value);
20
+ }
21
+ function defineBridge(entries) {
22
+ const bridge = {};
23
+ for (const [key, entry] of Object.entries(entries)) {
24
+ bridge[key] = async (...handlerArgs) => {
25
+ if (entry.args)
26
+ handlerArgs[0] = entry.args.parse(handlerArgs[0]);
27
+ return entry.handler(...handlerArgs);
28
+ };
29
+ }
30
+ return bridge;
31
+ }
32
+ const NO_ARGS_SCHEMA = { type: 'object', properties: {}, additionalProperties: false };
33
+ function toToolInputSchema(args) {
34
+ if (!args)
35
+ return { ...NO_ARGS_SCHEMA };
36
+ const s = schemaToJSONSchema(args);
37
+ if (!s || s.type !== 'object')
38
+ return { ...NO_ARGS_SCHEMA };
39
+ return s;
40
+ }
41
+ function schemaToJSONSchema(schema) {
42
+ const jsonSchema = zod_1.z.toJSONSchema(schema, {
43
+ unrepresentable: 'any',
44
+ override: (ctx) => {
45
+ // z.date() is unrepresentable in JSON Schema — emit ISO 8601 string
46
+ if (ctx.zodSchema?._zod?.def?.type === 'date') {
47
+ ctx.jsonSchema.type = 'string';
48
+ ctx.jsonSchema.format = 'date-time';
49
+ }
50
+ }
51
+ });
52
+ delete jsonSchema['$schema'];
53
+ return jsonSchema;
54
+ }
55
+ // --- Direct tool generation (attach all tools to LLM) ---
56
+ function toLLMTools(entries, options) {
57
+ const format = options?.format || 'openai';
58
+ const includeResponse = options?.includeResponse || false;
59
+ // Guard against invalid formats slipping in via casts (e.g. from query params)
60
+ if (!isLLMToolFormat(format))
61
+ throw new Error(`Invalid LLM tool format: ${format}. Expected one of ${exports.LLM_TOOL_FORMATS.join(', ')}`);
62
+ const tools = [];
63
+ for (const [name, entry] of Object.entries(entries)) {
64
+ if (entry.llm === false)
65
+ continue;
66
+ const description = entry.description;
67
+ const parameters = toToolInputSchema(entry.args);
68
+ let response;
69
+ if (includeResponse)
70
+ response = schemaToJSONSchema(entry.res);
71
+ switch (format) {
72
+ case 'openai':
73
+ tools.push({
74
+ type: 'function',
75
+ function: { name, description, parameters, ...(response ? { response } : {}) }
76
+ });
77
+ break;
78
+ case 'anthropic':
79
+ tools.push({
80
+ name,
81
+ description,
82
+ input_schema: parameters,
83
+ ...(response ? { output_schema: response } : {})
84
+ });
85
+ break;
86
+ case 'json-schema':
87
+ tools.push({ name, description, parameters, ...(response ? { response } : {}) });
88
+ break;
89
+ default: {
90
+ const exhaustiveCheck = format;
91
+ throw new Error(`Unhandled LLM tool format: ${exhaustiveCheck}`);
92
+ }
93
+ }
94
+ }
95
+ return tools;
96
+ }
97
+ // --- Meta-tools (tool_search → tool_describe → tool_use) ---
98
+ function getMetaTools(options) {
99
+ const format = options?.format || 'openai';
100
+ const searchTool = {
101
+ name: 'tool_search',
102
+ description: 'Search for available tools by keyword. Returns matching tool names and descriptions. Use tool_describe to get the full schema before calling tool_use.',
103
+ parameters: {
104
+ type: 'object',
105
+ properties: {
106
+ query: {
107
+ type: 'string',
108
+ description: 'Case-insensitive search query matched against tool names and descriptions'
109
+ }
110
+ }
111
+ }
112
+ };
113
+ const describeTool = {
114
+ name: 'tool_describe',
115
+ description: 'Get the full input and output schema for a tool. Call this after tool_search and before tool_use.',
116
+ parameters: {
117
+ type: 'object',
118
+ properties: {
119
+ name: {
120
+ type: 'string',
121
+ description: 'The tool name returned by tool_search (e.g., "user.fetch")'
122
+ }
123
+ },
124
+ required: ['name']
125
+ }
126
+ };
127
+ const useTool = {
128
+ name: 'tool_use',
129
+ description: 'Execute a tool by name. Use tool_search and tool_describe first to discover tools and their schemas.',
130
+ parameters: {
131
+ type: 'object',
132
+ properties: {
133
+ name: {
134
+ type: 'string',
135
+ description: 'The tool name to execute'
136
+ },
137
+ arguments: {
138
+ type: 'object',
139
+ description: 'Arguments matching the input schema from tool_describe'
140
+ }
141
+ },
142
+ required: ['name']
143
+ }
144
+ };
145
+ const tools = [searchTool, describeTool, useTool];
146
+ switch (format) {
147
+ case 'openai':
148
+ return tools.map(t => ({ type: 'function', function: t }));
149
+ case 'anthropic':
150
+ return tools.map(t => ({ name: t.name, description: t.description, input_schema: t.parameters }));
151
+ default:
152
+ return tools;
153
+ }
154
+ }
155
+ function toolSearch(entries, query) {
156
+ const results = [];
157
+ const q = query?.toLowerCase();
158
+ for (const [name, entry] of Object.entries(entries)) {
159
+ if (entry.llm === false)
160
+ continue;
161
+ if (q) {
162
+ const haystack = `${name} ${entry.description || ''}`.toLowerCase();
163
+ if (!haystack.includes(q))
164
+ continue;
165
+ }
166
+ results.push({ name, description: entry.description });
167
+ }
168
+ return results;
169
+ }
170
+ function toolDescribe(entries, name) {
171
+ const entry = entries[name];
172
+ if (!entry || entry.llm === false)
173
+ throw new Error(`Tool not found: ${name}`);
174
+ return {
175
+ name,
176
+ description: entry.description,
177
+ args: toToolInputSchema(entry.args),
178
+ response: schemaToJSONSchema(entry.res)
179
+ };
180
+ }
181
+ /**
182
+ * Serialize a tool result and enforce `tbConfig.maxToolOutputChars`. Oversized results
183
+ * throw (instead of truncating to invalid JSON) so the model is prompted to narrow its
184
+ * query. Returns the serialized JSON so callers can reuse it without re-stringifying.
185
+ */
186
+ function enforceToolOutputLimit(result) {
187
+ const serialized = JSON.stringify(result) ?? '';
188
+ const limit = config_1.config.maxToolOutputChars;
189
+ if (limit > 0 && serialized.length > limit) {
190
+ throw new Error(`Result too large (${serialized.length} chars, limit ${limit}). Narrow the query with filters or pagination.`);
191
+ }
192
+ return serialized;
193
+ }
194
+ async function toolUse(bridge, name, args, context) {
195
+ const handler = bridge[name];
196
+ if (!handler)
197
+ throw new Error(`Tool not found: ${name}`);
198
+ const result = await handler(args, context);
199
+ enforceToolOutputLimit(result);
200
+ return result;
201
+ }
202
+ async function handleMetaToolCall(bridge, entries, toolCall, context) {
203
+ switch (toolCall.name) {
204
+ case 'tool_search':
205
+ return toolSearch(entries, toolCall.arguments?.query);
206
+ case 'tool_describe':
207
+ return toolDescribe(entries, toolCall.arguments?.name);
208
+ case 'tool_use': {
209
+ const args = toolCall.arguments;
210
+ const entry = entries[args.name];
211
+ if (!entry || entry.llm === false)
212
+ throw new Error(`Tool not found: ${args.name}`);
213
+ return toolUse(bridge, args.name, args.arguments || {}, context);
214
+ }
215
+ default:
216
+ throw new Error(`Unknown meta-tool: ${toolCall.name}. Expected "tool_search", "tool_describe", or "tool_use".`);
217
+ }
218
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "typed-bridge",
3
- "description": "Strictly typed server functions for typescript apps",
4
- "version": "2.1.4",
3
+ "description": "Type-safe server functions for TypeScript that also expose your backend as an MCP server and LLM tools for AI agents",
4
+ "version": "3.0.0",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "author": "neilveil",
@@ -13,13 +13,15 @@
13
13
  "test:server": "tsx --watch src/demo/index.ts",
14
14
  "test:cli": "tsx src/scripts/cli.ts gen-typed-bridge-client --src ./src/demo/bridge/index.ts --dest ./test/bridge.ts",
15
15
  "test:bridge": "tsx test/index.ts",
16
+ "test:llm": "tsx test/llm.ts",
16
17
  "lint": "eslint",
17
- "docs": "cat readme.md docs/* > docs.md"
18
+ "prepublishOnly": "npm run dist"
18
19
  },
19
20
  "bin": {
20
21
  "typed-bridge": "./dist/scripts/cli.js"
21
22
  },
22
23
  "dependencies": {
24
+ "@modelcontextprotocol/sdk": "^1.29.0",
23
25
  "chalk": "^5.4.1",
24
26
  "commander": "^13.1.0",
25
27
  "compression": "^1.8.0",
@@ -38,10 +40,12 @@
38
40
  "@types/node": "^22.14.1",
39
41
  "@typescript-eslint/eslint-plugin": "^8.30.1",
40
42
  "@typescript-eslint/parser": "^8.30.1",
43
+ "dotenv": "^17.4.2",
41
44
  "eslint": "^9.25.0",
42
45
  "eslint-plugin-node": "^11.1.0",
43
46
  "globals": "^16.0.0",
44
47
  "nodemon": "^3.1.9",
48
+ "openai": "^6.42.0",
45
49
  "prettier": "^3.5.3",
46
50
  "rimraf": "^6.0.1",
47
51
  "tsconfig-paths": "^4.2.0",
@@ -49,15 +53,26 @@
49
53
  "typescript-eslint": "^8.30.1"
50
54
  },
51
55
  "keywords": [
56
+ "agents",
57
+ "ai",
58
+ "anthropic",
52
59
  "api",
53
60
  "backend",
54
- "development",
55
61
  "framework",
56
62
  "functions",
57
63
  "grpc",
64
+ "llm",
65
+ "mcp",
66
+ "model-context-protocol",
67
+ "openai",
68
+ "rpc",
58
69
  "server",
70
+ "tool-calling",
71
+ "tools",
59
72
  "trpc",
73
+ "type-safe",
60
74
  "typed",
75
+ "typescript",
61
76
  "validation",
62
77
  "zod"
63
78
  ]