typed-bridge 2.1.5 → 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.
- package/dist/bridge/index.d.ts +9 -4
- package/dist/bridge/index.js +32 -4
- package/dist/config/index.d.ts +1 -0
- package/dist/config/index.js +2 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11 -1
- package/dist/mcp/index.d.ts +5 -0
- package/dist/mcp/index.js +119 -0
- package/dist/scripts/buildTypeBridge.js +34 -1
- package/dist/scripts/typedBridgeCleaner.js +222 -29
- package/dist/tools/index.d.ts +126 -0
- package/dist/tools/index.js +218 -0
- package/package.json +18 -3
- package/readme.md +203 -234
- package/.vscode/settings.json +0 -3
- package/dist/demo/bridge/index.d.ts +0 -255
- package/dist/demo/bridge/index.js +0 -56
- package/dist/demo/bridge/order/index.d.ts +0 -42
- package/dist/demo/bridge/order/index.js +0 -165
- package/dist/demo/bridge/order/types.d.ts +0 -187
- package/dist/demo/bridge/order/types.js +0 -150
- package/dist/demo/bridge/product/index.d.ts +0 -16
- package/dist/demo/bridge/product/index.js +0 -67
- package/dist/demo/bridge/product/types.d.ts +0 -24
- package/dist/demo/bridge/product/types.js +0 -27
- package/dist/demo/bridge/user/index.d.ts +0 -28
- package/dist/demo/bridge/user/index.js +0 -86
- package/dist/demo/bridge/user/types.d.ts +0 -23
- package/dist/demo/bridge/user/types.js +0 -26
- package/dist/demo/index.d.ts +0 -1
- package/dist/demo/index.js +0 -23
- package/dist/demo/middleware.d.ts +0 -1
- package/dist/demo/middleware.js +0 -58
- package/test/bridge.ts +0 -283
- package/test/index.ts +0 -251
package/readme.md
CHANGED
|
@@ -1,363 +1,332 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="assets/default.png" alt="Typed Bridge" width="380" />
|
|
4
|
+
|
|
5
|
+
# Typed Bridge
|
|
6
|
+
|
|
7
|
+
### Write one function. Get a typed API, an MCP server, and LLM tools.
|
|
2
8
|
|
|
3
9
|
[](https://www.npmjs.com/package/typed-bridge)
|
|
4
10
|
[](https://www.npmjs.com/package/typed-bridge)
|
|
5
11
|
[](https://github.com/neilveil/typed-bridge/blob/main/license.txt)
|
|
6
12
|
|
|
7
|
-
**
|
|
13
|
+
**Type-safe RPC for humans. Native tools for AI. Zero glue code.**
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
</div>
|
|
10
16
|
|
|
11
17
|
---
|
|
12
18
|
|
|
13
|
-
##
|
|
19
|
+
## Your backend just became AI-native
|
|
14
20
|
|
|
15
|
-
|
|
21
|
+
You already write plain TypeScript functions on your server. Typed Bridge takes those exact functions and hands you three things at once:
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
1. A **fully typed client** your frontend calls like local functions.
|
|
24
|
+
2. An **MCP server** so Cursor, Claude Desktop, and Windsurf can call your backend directly.
|
|
25
|
+
3. **LLM tool definitions** so OpenAI and Anthropic can use your backend as tools.
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
Same function. Same validation. No second codebase for AI. No hand written tool schemas. No drift.
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
> Every function you ship is instantly something an agent can call. That is the whole pitch.
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
import { createBridge } from 'typed-bridge'
|
|
27
|
-
import bridge from './bridge'
|
|
31
|
+
---
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
## One function, three superpowers
|
|
34
|
+
|
|
35
|
+
```mermaid
|
|
36
|
+
flowchart LR
|
|
37
|
+
A[Your TypeScript functions] --> B[defineBridge]
|
|
38
|
+
B --> C[Typed client for your frontend]
|
|
39
|
+
B --> D[MCP server for AI tools]
|
|
40
|
+
B --> E[LLM tool definitions]
|
|
41
|
+
D --> F[Cursor, Claude, Windsurf]
|
|
42
|
+
E --> G[OpenAI, Anthropic, your agents]
|
|
30
43
|
```
|
|
31
44
|
|
|
32
|
-
|
|
45
|
+
You describe a function once with a Zod schema. Typed Bridge derives the client types, the MCP tool schema, and the LLM tool schema from that single source. They can never fall out of sync, because there is only one truth.
|
|
33
46
|
|
|
34
|
-
|
|
47
|
+
---
|
|
35
48
|
|
|
36
|
-
|
|
37
|
-
import * as user from './user'
|
|
49
|
+
## Quick start
|
|
38
50
|
|
|
39
|
-
|
|
40
|
-
'user.fetch': user.fetch,
|
|
41
|
-
'user.update': user.update,
|
|
42
|
-
'user.fetchAll': user.fetchAll
|
|
43
|
-
}
|
|
44
|
-
```
|
|
51
|
+
### 1. Install
|
|
45
52
|
|
|
46
|
-
|
|
53
|
+
```bash
|
|
54
|
+
npm i typed-bridge
|
|
55
|
+
```
|
|
47
56
|
|
|
48
|
-
|
|
57
|
+
### 2. Write a normal function
|
|
49
58
|
|
|
50
59
|
`bridge/user/index.ts`:
|
|
51
60
|
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
name: string
|
|
56
|
-
email: string
|
|
57
|
-
}
|
|
61
|
+
```ts
|
|
62
|
+
import { z } from 'typed-bridge'
|
|
63
|
+
import * as types from './types'
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
export const fetch = async (args: { id: number }): Promise<User> => {
|
|
65
|
+
export const fetch = async (args: z.infer<typeof types.fetch.args>) => {
|
|
61
66
|
return db.users.findById(args.id)
|
|
62
67
|
}
|
|
63
|
-
|
|
64
|
-
// Update user fields
|
|
65
|
-
export const update = async (args: { id: number; name?: string; email?: string }): Promise<User> => {
|
|
66
|
-
return db.users.update(args.id, args)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// List all users
|
|
70
|
-
export const fetchAll = async (): Promise<User[]> => {
|
|
71
|
-
return db.users.findAll()
|
|
72
|
-
}
|
|
73
68
|
```
|
|
74
69
|
|
|
75
|
-
###
|
|
70
|
+
### 3. Describe it once
|
|
76
71
|
|
|
77
|
-
|
|
72
|
+
`bridge/user/types.ts`:
|
|
78
73
|
|
|
79
|
-
```
|
|
80
|
-
{
|
|
81
|
-
"scripts": {
|
|
82
|
-
"gen:typed-bridge-client": "typed-bridge gen-typed-bridge-client --src ./src/bridge/index.ts --dest ./bridge.ts"
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Run it:
|
|
74
|
+
```ts
|
|
75
|
+
import { z } from 'typed-bridge'
|
|
88
76
|
|
|
89
|
-
|
|
90
|
-
|
|
77
|
+
export const fetch = {
|
|
78
|
+
description: 'Fetch a user by ID',
|
|
79
|
+
args: z.object({ id: z.number().min(1).describe('Unique user identifier') }),
|
|
80
|
+
res: z.object({ id: z.number(), name: z.string(), email: z.string() })
|
|
81
|
+
}
|
|
91
82
|
```
|
|
92
83
|
|
|
93
|
-
###
|
|
84
|
+
### 4. Wire it up with `defineBridge`
|
|
94
85
|
|
|
95
|
-
|
|
86
|
+
`bridge/index.ts`:
|
|
96
87
|
|
|
97
|
-
```
|
|
98
|
-
import
|
|
88
|
+
```ts
|
|
89
|
+
import { defineBridge } from 'typed-bridge'
|
|
90
|
+
import * as user from './user'
|
|
91
|
+
import * as userTypes from './user/types'
|
|
99
92
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
'Content-Type': 'application/json',
|
|
103
|
-
Authorization: 'Bearer 123'
|
|
104
|
-
}
|
|
105
|
-
typedBridgeConfig.onResponse = res => {
|
|
106
|
-
// Custom response handling
|
|
93
|
+
export const entries = {
|
|
94
|
+
'user.fetch': { handler: user.fetch, ...userTypes.fetch }
|
|
107
95
|
}
|
|
108
96
|
|
|
109
|
-
|
|
97
|
+
export default defineBridge(entries)
|
|
110
98
|
```
|
|
111
99
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
## Keeping the Client in Sync
|
|
100
|
+
`defineBridge` auto-validates incoming args against your Zod schema. No manual `.parse()` in handlers, ever.
|
|
115
101
|
|
|
116
|
-
|
|
102
|
+
### 5. Boot the server, AI included
|
|
117
103
|
|
|
118
|
-
|
|
104
|
+
```ts
|
|
105
|
+
import { createBridge } from 'typed-bridge'
|
|
106
|
+
import bridge, { entries } from './bridge'
|
|
119
107
|
|
|
120
|
-
|
|
108
|
+
createBridge(bridge, 8080, '/bridge', { entries, mcp: true })
|
|
109
|
+
```
|
|
121
110
|
|
|
122
|
-
|
|
111
|
+
That is it. You now have a typed HTTP API, an MCP endpoint at `/bridge/mcp`, and an LLM tools endpoint at `/bridge/tools`. From one function.
|
|
123
112
|
|
|
124
113
|
---
|
|
125
114
|
|
|
126
|
-
##
|
|
115
|
+
## Superpower 1: The typed client
|
|
127
116
|
|
|
128
|
-
|
|
117
|
+
Generate a standalone client file for your frontend:
|
|
129
118
|
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
console.log('Middleware for user.fetch')
|
|
133
|
-
})
|
|
119
|
+
```bash
|
|
120
|
+
typed-bridge gen-typed-bridge-client --src ./src/bridge/index.ts --dest ./bridge.ts
|
|
134
121
|
```
|
|
135
122
|
|
|
136
|
-
|
|
123
|
+
Call your backend like it lives in the same file:
|
|
137
124
|
|
|
138
125
|
```ts
|
|
139
|
-
|
|
140
|
-
console.log('Middleware for all user routes')
|
|
141
|
-
})
|
|
142
|
-
```
|
|
126
|
+
import bridge, { typedBridgeConfig } from './bridge'
|
|
143
127
|
|
|
144
|
-
|
|
128
|
+
typedBridgeConfig.host = 'http://localhost:8080/bridge'
|
|
145
129
|
|
|
146
|
-
|
|
147
|
-
* → runs first (matches everything)
|
|
148
|
-
user.* → runs second
|
|
149
|
-
user.fetch → runs last (most specific)
|
|
130
|
+
const user = await bridge['user.fetch']({ id: 1 })
|
|
150
131
|
```
|
|
151
132
|
|
|
152
|
-
|
|
133
|
+
Full autocomplete, full type safety, from server to screen. Your frontend never imports Zod and never sees your backend code. It is one generated file you can drop into React, Vue, Angular, React Native, or anything else.
|
|
153
134
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
```ts
|
|
157
|
-
createMiddleware('user.*', async (req, res) => {
|
|
158
|
-
return {
|
|
159
|
-
context: {
|
|
160
|
-
a: 1
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
})
|
|
135
|
+
---
|
|
164
136
|
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
context: {
|
|
168
|
-
b: 2
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
})
|
|
172
|
-
```
|
|
137
|
+
## Superpower 2: The MCP server (the headline act)
|
|
173
138
|
|
|
174
|
-
|
|
139
|
+
Flip one flag and your backend becomes a [Model Context Protocol](https://modelcontextprotocol.io) server. AI tools connect to it and call your real functions, with your real validation.
|
|
175
140
|
|
|
176
141
|
```ts
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
b: number
|
|
180
|
-
}
|
|
142
|
+
createBridge(bridge, 8080, '/bridge', { entries, mcp: true })
|
|
143
|
+
```
|
|
181
144
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
145
|
+
Point any MCP client at it:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"mcpServers": {
|
|
150
|
+
"my-backend": {
|
|
151
|
+
"url": "http://localhost:8080/bridge/mcp",
|
|
152
|
+
"headers": { "Authorization": "Bearer ${MCP_API_KEY}" },
|
|
153
|
+
"env": { "MCP_API_KEY": "your-api-key" }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
188
156
|
}
|
|
189
157
|
```
|
|
190
158
|
|
|
191
|
-
###
|
|
159
|
+
### Auth that actually works
|
|
192
160
|
|
|
193
|
-
|
|
161
|
+
MCP requests skip your normal middleware, so you derive context straight from headers:
|
|
194
162
|
|
|
195
163
|
```ts
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
164
|
+
createBridge(bridge, 8080, '/bridge', {
|
|
165
|
+
entries,
|
|
166
|
+
mcp: true,
|
|
167
|
+
mcpGetContext: async headers => {
|
|
168
|
+
const user = await verifyToken(headers['authorization'])
|
|
169
|
+
return { userId: user.id, role: user.role }
|
|
199
170
|
}
|
|
200
171
|
})
|
|
201
172
|
```
|
|
202
173
|
|
|
203
|
-
|
|
174
|
+
The returned context lands in every handler as the second argument, exactly like middleware context. Same security model for humans and agents.
|
|
204
175
|
|
|
205
|
-
|
|
206
|
-
createMiddleware('user.*', async (req, res) => {
|
|
207
|
-
if (req.headers.authorization !== 'Bearer 123') {
|
|
208
|
-
res.status(401).send('Unauthorized')
|
|
176
|
+
### Choose what each surface can touch
|
|
209
177
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
178
|
+
MCP and LLM tools are independent surfaces. Every entry is exposed to both by default, and two flags let you hide a handler from either one while it stays fully callable over HTTP:
|
|
179
|
+
|
|
180
|
+
- `mcp: false` keeps a handler off the MCP server.
|
|
181
|
+
- `llm: false` keeps it out of `toLLMTools`, tool search, and the `/bridge/tools` endpoint.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
export const entries = {
|
|
185
|
+
'user.fetch': { handler: user.fetch, ...userTypes.fetch },
|
|
186
|
+
'user.remove': { handler: user.remove, ...userTypes.remove, mcp: false }, // your LLM app can call it, external MCP clients cannot
|
|
187
|
+
'admin.sync': { handler: admin.sync, ...adminTypes.sync, llm: false } // HTTP and MCP only, never an LLM tool
|
|
188
|
+
}
|
|
214
189
|
```
|
|
215
190
|
|
|
216
|
-
|
|
191
|
+
Hidden tools are dropped from discovery and rejected if called by name, so a model cannot reach them even by guessing.
|
|
217
192
|
|
|
218
|
-
|
|
193
|
+
---
|
|
219
194
|
|
|
220
|
-
|
|
195
|
+
## Superpower 3: LLM tool calling
|
|
221
196
|
|
|
222
|
-
|
|
197
|
+
Skip MCP and talk to models directly. Typed Bridge speaks OpenAI, Anthropic, and raw JSON Schema.
|
|
223
198
|
|
|
224
|
-
|
|
199
|
+
### Hand every tool to the model
|
|
225
200
|
|
|
226
201
|
```ts
|
|
227
|
-
import {
|
|
202
|
+
import { toLLMTools } from 'typed-bridge'
|
|
228
203
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
id: z.number().min(1)
|
|
232
|
-
}),
|
|
233
|
-
res: z.object({
|
|
234
|
-
id: z.number(),
|
|
235
|
-
name: z.string()
|
|
236
|
-
})
|
|
237
|
-
}
|
|
204
|
+
const tools = toLLMTools(entries, { format: 'openai' })
|
|
205
|
+
// Pass `tools` straight into openai.chat.completions.create()
|
|
238
206
|
```
|
|
239
207
|
|
|
240
|
-
|
|
208
|
+
Formats: `openai`, `anthropic`, `json-schema`.
|
|
209
|
+
|
|
210
|
+
### Have a giant API? Use meta-tools.
|
|
211
|
+
|
|
212
|
+
For hundreds of endpoints, do not flood the context window. Give the model three tools instead of two hundred:
|
|
241
213
|
|
|
242
214
|
```ts
|
|
243
|
-
import {
|
|
244
|
-
import * as types from './types'
|
|
215
|
+
import { getMetaTools, handleMetaToolCall } from 'typed-bridge'
|
|
245
216
|
|
|
246
|
-
|
|
247
|
-
args: z.infer<typeof types.fetch.args>,
|
|
248
|
-
context: { id: number }
|
|
249
|
-
): Promise<z.infer<typeof types.fetch.res>> => {
|
|
250
|
-
args = types.fetch.args.parse(args)
|
|
217
|
+
const tools = getMetaTools({ format: 'openai' })
|
|
251
218
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
219
|
+
// The model discovers tools, inspects their schema, then calls them:
|
|
220
|
+
// 1. tool_search({ query: "user" }) → [{ name: "user.fetch", description: "..." }, ...]
|
|
221
|
+
// 2. tool_describe({ name: "user.fetch" }) → { name, description, args, response }
|
|
222
|
+
// 3. tool_use({ name: "user.fetch", arguments: { id: 1 } }) → { id: 1, name: "Alice" }
|
|
256
223
|
|
|
257
|
-
|
|
258
|
-
|
|
224
|
+
const result = await handleMetaToolCall(bridge, entries, {
|
|
225
|
+
name: 'tool_use',
|
|
226
|
+
arguments: { name: 'user.fetch', arguments: { id: 1 } }
|
|
227
|
+
})
|
|
259
228
|
```
|
|
260
229
|
|
|
261
|
-
The
|
|
230
|
+
The model calls `tool_search` to discover what exists (names and descriptions only), `tool_describe` to get the full schema for the tool it needs, then `tool_use` to run it. Your token bill stays flat as your API grows.
|
|
262
231
|
|
|
263
|
-
|
|
232
|
+
### Or just hit the REST endpoint
|
|
264
233
|
|
|
265
|
-
|
|
234
|
+
```
|
|
235
|
+
GET /bridge/tools?format=openai
|
|
236
|
+
```
|
|
266
237
|
|
|
267
|
-
|
|
268
|
-
import { tbConfig } from 'typed-bridge'
|
|
238
|
+
---
|
|
269
239
|
|
|
270
|
-
|
|
271
|
-
tbConfig.logs.response = true
|
|
272
|
-
tbConfig.logs.error = true
|
|
240
|
+
## Why Typed Bridge
|
|
273
241
|
|
|
274
|
-
|
|
275
|
-
tbConfig.logs.contextOnError = true
|
|
242
|
+
### vs writing AI tools by hand
|
|
276
243
|
|
|
277
|
-
|
|
278
|
-
|
|
244
|
+
| | **Typed Bridge** | **DIY tool calling** |
|
|
245
|
+
| ------------------------ | ----------------------------------------- | -------------------------------------------- |
|
|
246
|
+
| Tool schemas | Derived from your Zod types | Hand written and kept in sync manually |
|
|
247
|
+
| MCP server | One flag | A separate service to build and maintain |
|
|
248
|
+
| Validation | Shared with your API | Re-implemented for the AI path |
|
|
249
|
+
| Drift between code and AI | Impossible, single source | Constant, two sources |
|
|
279
250
|
|
|
280
|
-
|
|
251
|
+
### vs tRPC
|
|
281
252
|
|
|
282
|
-
|
|
253
|
+
| | **Typed Bridge** | **tRPC** |
|
|
254
|
+
| ---------------------- | --------------------------------------------- | ----------------------------------- |
|
|
255
|
+
| Setup | Plain functions, generate a client, done | Routers, procedures, adapters |
|
|
256
|
+
| Monorepo required | No, the client is a standalone file | Practically yes for type inference |
|
|
257
|
+
| Frontend framework | Any | React first, adapters for others |
|
|
258
|
+
| AI tooling | Built in (MCP and LLM) | Not included |
|
|
283
259
|
|
|
284
|
-
|
|
260
|
+
### vs GraphQL
|
|
261
|
+
|
|
262
|
+
| | **Typed Bridge** | **GraphQL** |
|
|
263
|
+
| ------------------ | ----------------------------------------- | -------------------------------------------- |
|
|
264
|
+
| Setup | Define functions, generate client | Schema, resolvers, codegen |
|
|
265
|
+
| Type safety | Automatic from signatures | Requires a codegen toolchain |
|
|
266
|
+
| Learning curve | Minimal, plain TypeScript | SDL, resolvers, fragments, queries |
|
|
267
|
+
| AI tooling | Built in | Roll your own |
|
|
285
268
|
|
|
286
|
-
|
|
287
|
-
import { createBridge, onShutdown } from 'typed-bridge'
|
|
288
|
-
import path from 'path'
|
|
289
|
-
import bridge from './bridge'
|
|
269
|
+
---
|
|
290
270
|
|
|
291
|
-
|
|
271
|
+
## Middleware when you need it
|
|
292
272
|
|
|
293
|
-
|
|
294
|
-
app.use(express.static(path.join(__dirname, 'public')))
|
|
273
|
+
Pattern based middleware runs before handlers and can inject context:
|
|
295
274
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
res.json({ status: 'ok', uptime: process.uptime() })
|
|
299
|
-
})
|
|
275
|
+
```ts
|
|
276
|
+
import { createMiddleware } from 'typed-bridge'
|
|
300
277
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
278
|
+
createMiddleware('user.*', async (req, res) => {
|
|
279
|
+
if (!req.headers.authorization) {
|
|
280
|
+
res.status(401).send('Unauthorized')
|
|
281
|
+
return { next: false }
|
|
282
|
+
}
|
|
283
|
+
return { context: { userId: 1 } }
|
|
304
284
|
})
|
|
305
285
|
```
|
|
306
286
|
|
|
307
|
-
|
|
287
|
+
Broader patterns run first (`*`, then `user.*`, then `user.fetch`). Returned context is merged and passed to the handler.
|
|
308
288
|
|
|
309
|
-
|
|
289
|
+
---
|
|
310
290
|
|
|
311
|
-
|
|
291
|
+
## Configuration
|
|
312
292
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
| **Setup** | Define functions, generate typed client, done | Routers, procedures, adapters |
|
|
316
|
-
| **Monorepo required?** | No, generated client is a standalone file | Practically yes, for type inference |
|
|
317
|
-
| **Frontend framework** | Any (React, Vue, Angular, RN, etc.) | React-first, adapters for others |
|
|
318
|
-
| **Learning curve** | Minimal, plain async functions | Moderate, procedures, context, middleware patterns |
|
|
319
|
-
| **Runtime validation** | Zod (built-in) | Zod or others via `.input()` |
|
|
293
|
+
```ts
|
|
294
|
+
import { tbConfig } from 'typed-bridge'
|
|
320
295
|
|
|
321
|
-
|
|
296
|
+
tbConfig.logs.request = true
|
|
297
|
+
tbConfig.logs.response = true
|
|
298
|
+
tbConfig.logs.error = true
|
|
299
|
+
tbConfig.responseDelay = 0 // Artificial delay in ms for testing loading states
|
|
300
|
+
tbConfig.maxToolOutputChars = 0 // Cap MCP/LLM tool results (chars of JSON); 0 = unlimited
|
|
301
|
+
```
|
|
322
302
|
|
|
323
|
-
|
|
324
|
-
| ------------------ | ------------------------------------------------------ | ---------------------------------------------------------- |
|
|
325
|
-
| **Setup** | Define functions, generate typed client, done | Schema definition, resolvers, codegen |
|
|
326
|
-
| **Type safety** | Automatic from function signatures | Requires codegen toolchain (e.g. GraphQL Code Generator) |
|
|
327
|
-
| **Overfetching** | Not applicable, you control what each function returns | Solved by design with field selection |
|
|
328
|
-
| **Learning curve** | Minimal, plain TypeScript | Significant: SDL, resolvers, fragments, queries, mutations |
|
|
329
|
-
| **Best for** | App-specific backends, internal APIs | Public APIs, multi-client data graphs |
|
|
303
|
+
`createBridge` also returns the underlying Express `app` and `server`, so you can add routes, serve static files, or attach any Express middleware.
|
|
330
304
|
|
|
331
|
-
|
|
305
|
+
### Guarding tool output size
|
|
332
306
|
|
|
333
|
-
|
|
307
|
+
The same function can serve a data-heavy response over HTTP and as an AI tool. A frontend handles a large payload fine, but feeding it to a model wastes tokens or overflows the context window. Set `tbConfig.maxToolOutputChars` to cap the serialized result on the **MCP and LLM tool surfaces only** (HTTP is never limited). Oversized results are **rejected, not truncated** — the caller gets an error telling it to narrow the query, so the model never receives invalid JSON:
|
|
334
308
|
|
|
335
|
-
|
|
309
|
+
```ts
|
|
310
|
+
tbConfig.maxToolOutputChars = 100_000
|
|
336
311
|
|
|
312
|
+
// A tool returning more than that responds with:
|
|
313
|
+
// "Result too large (182431 chars, limit 100000). Narrow the query with filters or pagination."
|
|
337
314
|
```
|
|
338
|
-
src/
|
|
339
|
-
server.ts Server entry
|
|
340
|
-
middleware.ts Middleware registrations (side-effect import)
|
|
341
|
-
bridge/
|
|
342
|
-
index.ts Route map (flat object)
|
|
343
|
-
user/
|
|
344
|
-
index.ts Handler functions
|
|
345
|
-
types.ts Zod schemas (optional)
|
|
346
|
-
product/
|
|
347
|
-
index.ts Handler functions
|
|
348
|
-
types.ts Zod schemas (optional)
|
|
349
|
-
```
|
|
350
315
|
|
|
351
|
-
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Adding a new route
|
|
352
319
|
|
|
353
|
-
1. Create handler in `bridge/<module>/index.ts
|
|
354
|
-
2.
|
|
355
|
-
3. Register
|
|
356
|
-
|
|
357
|
-
|
|
320
|
+
1. Create the handler in `bridge/<module>/index.ts`.
|
|
321
|
+
2. Add its Zod schema in `<module>/types.ts`.
|
|
322
|
+
3. Register it in `bridge/index.ts`:
|
|
323
|
+
- Flat map for a plain typed API: `export default { 'module.action': module.action }`
|
|
324
|
+
- Entry based for AI features: `export const entries = { 'module.action': { handler: module.action, ...moduleTypes.action } }` then `export default defineBridge(entries)`
|
|
325
|
+
4. Add middleware if needed and import it in your server entry.
|
|
326
|
+
5. Regenerate the client.
|
|
358
327
|
|
|
359
328
|
---
|
|
360
329
|
|
|
361
330
|
## Developer
|
|
362
331
|
|
|
363
|
-
|
|
332
|
+
Built and maintained by [neilveil](https://github.com/neilveil). If Typed Bridge saves you a codebase, drop a star.
|
package/.vscode/settings.json
DELETED