orpc-fn 0.2.0 → 0.4.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 +28 -8
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +56 -0
- package/dist/cli.js.map +1 -0
- package/dist/create-fn.d.ts +3 -3
- package/dist/create-fn.d.ts.map +1 -1
- package/dist/create-fn.js +2 -7
- package/dist/create-fn.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/live/fn-live.d.ts +9 -5
- package/dist/live/fn-live.d.ts.map +1 -1
- package/dist/live/fn-live.js.map +1 -1
- package/dist/live/pub-sub.d.ts +5 -1
- package/dist/live/pub-sub.d.ts.map +1 -1
- package/dist/live/pub-sub.js +29 -14
- package/dist/live/pub-sub.js.map +1 -1
- package/dist/mcp/core.d.ts +41 -0
- package/dist/mcp/core.d.ts.map +1 -0
- package/dist/mcp/core.js +61 -0
- package/dist/mcp/core.js.map +1 -0
- package/dist/mcp/sdk.d.ts +8 -16
- package/dist/mcp/sdk.d.ts.map +1 -1
- package/dist/mcp/sdk.js +10 -78
- package/dist/mcp/sdk.js.map +1 -1
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +13 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/otel.d.ts +5 -0
- package/dist/otel.d.ts.map +1 -1
- package/dist/otel.js +7 -0
- package/dist/otel.js.map +1 -1
- package/dist/router.d.ts +8 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +13 -0
- package/dist/router.js.map +1 -1
- package/dist/types.d.ts +16 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +15 -3
- package/src/cli.ts +68 -0
- package/src/create-fn.ts +5 -10
- package/src/index.ts +2 -1
- package/src/live/fn-live.ts +9 -5
- package/src/live/pub-sub.ts +38 -19
- package/src/mcp/core.ts +102 -0
- package/src/mcp/sdk.ts +20 -102
- package/src/mcp/server.ts +35 -0
- package/src/otel.ts +8 -0
- package/src/router.ts +19 -0
- package/src/types.ts +24 -42
package/src/live/fn-live.ts
CHANGED
|
@@ -66,10 +66,14 @@ export type FnLiveConfig<
|
|
|
66
66
|
ChannelQueueOptions & {
|
|
67
67
|
/** Subscribe route name; default `${name}.live`. */
|
|
68
68
|
name?: string
|
|
69
|
-
/**
|
|
70
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Validates reducer state without reapplying an output transform. Required with a
|
|
71
|
+
* transformer. Its input may be looser than its output (e.g. branded IDs); it is
|
|
72
|
+
* checked at runtime.
|
|
73
|
+
*/
|
|
74
|
+
stateSchema?: Schema<unknown, TOutput>
|
|
71
75
|
/** Validates patch payloads. Required when a transformer returns fnLivePatch. */
|
|
72
|
-
emitSchema?: Schema<
|
|
76
|
+
emitSchema?: Schema<unknown, TEmit>
|
|
73
77
|
/** Optional access check before each update; idle streams keep their initial authorization. */
|
|
74
78
|
reauthorize?: AuthFn<TInput, FnContext<TDef, TKey>>
|
|
75
79
|
eventSchema: TEventSchema
|
|
@@ -122,10 +126,10 @@ export type FnLiveConfig<
|
|
|
122
126
|
TEmit,
|
|
123
127
|
TErrors
|
|
124
128
|
>
|
|
125
|
-
stateSchema: Schema<
|
|
129
|
+
stateSchema: Schema<unknown, TOutput>
|
|
126
130
|
}
|
|
127
131
|
) &
|
|
128
|
-
([TEmit] extends [never] ? object : { emitSchema: Schema<
|
|
132
|
+
([TEmit] extends [never] ? object : { emitSchema: Schema<unknown, TEmit> })
|
|
129
133
|
|
|
130
134
|
type FnLiveConfigTransformer<
|
|
131
135
|
TDef extends FnDefinition,
|
package/src/live/pub-sub.ts
CHANGED
|
@@ -2,7 +2,12 @@ import type { Schema } from '@orpc/contract'
|
|
|
2
2
|
import { ORPCError } from '@orpc/server'
|
|
3
3
|
import type { ZodObject, ZodRawShape, ZodType, z } from 'zod'
|
|
4
4
|
import type { FnLogger } from '../logger.js'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
errorAttributes,
|
|
7
|
+
errorMessageOf,
|
|
8
|
+
type SpanLike,
|
|
9
|
+
type Tracing
|
|
10
|
+
} from '../otel.js'
|
|
6
11
|
import type {
|
|
7
12
|
GuardOptions,
|
|
8
13
|
FnContext,
|
|
@@ -204,7 +209,11 @@ export type CreatePublisher = <TEventSchema extends ZodType>(
|
|
|
204
209
|
export type PubSubRuntimeOptions = {
|
|
205
210
|
/** Queue depths, drops, successful reconnects and parse failures. */
|
|
206
211
|
onMetric?: (event: PubSubMetric) => void
|
|
207
|
-
|
|
212
|
+
/**
|
|
213
|
+
* The broker, or a function creating it on first use so importing routes
|
|
214
|
+
* opens no connection: `transport: () => bunRedisTransport(getRedis())`.
|
|
215
|
+
*/
|
|
216
|
+
transport: PubSubTransport | (() => PubSubTransport)
|
|
208
217
|
/**
|
|
209
218
|
* Called per event dropped from ingress, replay or delivery queues.
|
|
210
219
|
* Delivery drops count events before subscriber filtering.
|
|
@@ -525,13 +534,19 @@ export function createLiveRuntime(options: LiveRuntimeOptions) {
|
|
|
525
534
|
// the client's backoff reconnects to the new deployment.
|
|
526
535
|
const activeSubscriberCleanups = new Set<() => void>()
|
|
527
536
|
|
|
537
|
+
let transport: PubSubTransport | undefined
|
|
528
538
|
const requireTransport = () => {
|
|
529
539
|
if (!options.pubsub) {
|
|
530
540
|
throw new Error(
|
|
531
541
|
'orpc-fn: live routes need a transport, pass createFn({ pubsub: { transport } })'
|
|
532
542
|
)
|
|
533
543
|
}
|
|
534
|
-
return
|
|
544
|
+
if (transport) return transport
|
|
545
|
+
// A lazy transport is never created during or after shutdown.
|
|
546
|
+
assertOpen()
|
|
547
|
+
const configured = options.pubsub.transport
|
|
548
|
+
transport = typeof configured === 'function' ? configured() : configured
|
|
549
|
+
return transport
|
|
535
550
|
}
|
|
536
551
|
|
|
537
552
|
/**
|
|
@@ -696,8 +711,9 @@ export function createLiveRuntime(options: LiveRuntimeOptions) {
|
|
|
696
711
|
name: config.name,
|
|
697
712
|
channel
|
|
698
713
|
})
|
|
699
|
-
logger.warn(
|
|
700
|
-
|
|
714
|
+
logger.warn('pubsub.message_dropped', {
|
|
715
|
+
channel,
|
|
716
|
+
...errorAttributes(error)
|
|
701
717
|
})
|
|
702
718
|
}
|
|
703
719
|
}
|
|
@@ -711,7 +727,8 @@ export function createLiveRuntime(options: LiveRuntimeOptions) {
|
|
|
711
727
|
if (closed || timer) return
|
|
712
728
|
const delayMs = getResubscribeDelayMs(attempt)
|
|
713
729
|
attempt++
|
|
714
|
-
logger.warn(
|
|
730
|
+
logger.warn('pubsub.resubscribe_scheduled', {
|
|
731
|
+
channel,
|
|
715
732
|
reason,
|
|
716
733
|
attempt,
|
|
717
734
|
delayMs
|
|
@@ -739,8 +756,9 @@ export function createLiveRuntime(options: LiveRuntimeOptions) {
|
|
|
739
756
|
connected = false
|
|
740
757
|
markPending()
|
|
741
758
|
}
|
|
742
|
-
logger.error(
|
|
743
|
-
|
|
759
|
+
logger.error('pubsub.subscription_lost', {
|
|
760
|
+
channel,
|
|
761
|
+
...errorAttributes(error)
|
|
744
762
|
})
|
|
745
763
|
scheduleResubscribe('lost')
|
|
746
764
|
},
|
|
@@ -774,10 +792,11 @@ export function createLiveRuntime(options: LiveRuntimeOptions) {
|
|
|
774
792
|
connectedOnce = true
|
|
775
793
|
markReady()
|
|
776
794
|
attempt = 0
|
|
777
|
-
logger.
|
|
795
|
+
logger.debug('pubsub.subscribed', { channel })
|
|
778
796
|
} catch (error) {
|
|
779
|
-
logger.error(
|
|
780
|
-
|
|
797
|
+
logger.error('pubsub.subscribe_failed', {
|
|
798
|
+
channel,
|
|
799
|
+
...errorAttributes(error)
|
|
781
800
|
})
|
|
782
801
|
scheduleResubscribe('subscribe-failed')
|
|
783
802
|
}
|
|
@@ -916,7 +935,7 @@ export function createLiveRuntime(options: LiveRuntimeOptions) {
|
|
|
916
935
|
throw error
|
|
917
936
|
}
|
|
918
937
|
|
|
919
|
-
subscriberLogger.
|
|
938
|
+
subscriberLogger.debug('pubsub.subscribing', { channel: channelName })
|
|
920
939
|
|
|
921
940
|
let messageCount = 0
|
|
922
941
|
let droppedCount = 0
|
|
@@ -1041,7 +1060,8 @@ export function createLiveRuntime(options: LiveRuntimeOptions) {
|
|
|
1041
1060
|
span?.setAttribute('pubsub.message_count', messageCount)
|
|
1042
1061
|
span?.setAttribute('pubsub.dropped_count', droppedCount)
|
|
1043
1062
|
tracing.end(span, ...error)
|
|
1044
|
-
subscriberLogger.
|
|
1063
|
+
subscriberLogger.debug('pubsub.unsubscribed', {
|
|
1064
|
+
channel: channelName,
|
|
1045
1065
|
messageCount,
|
|
1046
1066
|
droppedCount,
|
|
1047
1067
|
durationMs: Math.round(durationMs)
|
|
@@ -1096,10 +1116,10 @@ export function createLiveRuntime(options: LiveRuntimeOptions) {
|
|
|
1096
1116
|
if (items.length > maxReplay) replayGap = true
|
|
1097
1117
|
} catch (error) {
|
|
1098
1118
|
replayGap = true
|
|
1099
|
-
subscriberLogger.warn(
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
)
|
|
1119
|
+
subscriberLogger.warn('pubsub.replay_failed', {
|
|
1120
|
+
channel: channelName,
|
|
1121
|
+
...errorAttributes(error)
|
|
1122
|
+
})
|
|
1103
1123
|
}
|
|
1104
1124
|
}
|
|
1105
1125
|
for (const { data, id } of replaying ?? [])
|
|
@@ -1241,8 +1261,7 @@ export function createLiveRuntime(options: LiveRuntimeOptions) {
|
|
|
1241
1261
|
shutdownPromise = (async () => {
|
|
1242
1262
|
await Promise.allSettled([...pendingOpens])
|
|
1243
1263
|
await Promise.all([...releases])
|
|
1244
|
-
if (options.pubsub?.ownsTransport)
|
|
1245
|
-
await options.pubsub.transport.close?.()
|
|
1264
|
+
if (options.pubsub?.ownsTransport) await transport?.close?.()
|
|
1246
1265
|
})()
|
|
1247
1266
|
return shutdownPromise
|
|
1248
1267
|
},
|
package/src/mcp/core.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyRouter,
|
|
3
|
+
type InferRouterInitialContext,
|
|
4
|
+
call
|
|
5
|
+
} from '@orpc/server'
|
|
6
|
+
import { isObjectJsonSchema } from '../json-schema.js'
|
|
7
|
+
import { listTools, type McpTool, type ProcedureFilter } from '../mcp.js'
|
|
8
|
+
|
|
9
|
+
/** Text-only tool result: the shape both MCP SDK majors accept. */
|
|
10
|
+
export type TextToolResult = {
|
|
11
|
+
content: { type: 'text'; text: string }[]
|
|
12
|
+
isError?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type RegisterMcpToolsOptions<R extends AnyRouter, TResult> = {
|
|
16
|
+
/** Tool exposure must be an explicit application choice. */
|
|
17
|
+
filter: ProcedureFilter
|
|
18
|
+
context: (request: {
|
|
19
|
+
name: string
|
|
20
|
+
signal: AbortSignal
|
|
21
|
+
}) => InferRouterInitialContext<R> | Promise<InferRouterInitialContext<R>>
|
|
22
|
+
readOnly?: ProcedureFilter
|
|
23
|
+
name?: NonNullable<Parameters<typeof listTools>[1]>['name']
|
|
24
|
+
formatResult?: (value: unknown, tool: McpTool) => TResult | Promise<TResult>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Default protocol result: JSON/text. Provide formatResult for other MCP content. */
|
|
28
|
+
export function formatMcpResult(value: unknown): TextToolResult {
|
|
29
|
+
const text =
|
|
30
|
+
typeof value === 'string'
|
|
31
|
+
? value
|
|
32
|
+
: (JSON.stringify(value, (_key, item) =>
|
|
33
|
+
typeof item === 'bigint' ? item.toString() : item
|
|
34
|
+
) ?? 'null')
|
|
35
|
+
return { content: [{ type: 'text', text }] }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const errorResult = (text: string): TextToolResult => ({
|
|
39
|
+
content: [{ type: 'text', text }],
|
|
40
|
+
isError: true
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
/** @internal SDK-independent `tools/list` and `tools/call` behind both adapters. */
|
|
44
|
+
export function createToolHandlers<R extends AnyRouter, TResult>(
|
|
45
|
+
router: R,
|
|
46
|
+
options: RegisterMcpToolsOptions<R, TResult>
|
|
47
|
+
) {
|
|
48
|
+
const definitions = listTools(router, {
|
|
49
|
+
filter: options.filter,
|
|
50
|
+
...(options.readOnly ? { readOnly: options.readOnly } : {}),
|
|
51
|
+
...(options.name ? { name: options.name } : {})
|
|
52
|
+
})
|
|
53
|
+
const tools = new Map(definitions.map((tool) => [tool.name, tool]))
|
|
54
|
+
const descriptions = definitions.map((tool) => {
|
|
55
|
+
const inputSchema = tool.config.inputSchema?.[
|
|
56
|
+
'~standard'
|
|
57
|
+
].jsonSchema.input() ?? { type: 'object', properties: {} }
|
|
58
|
+
if (!isObjectJsonSchema(inputSchema))
|
|
59
|
+
throw new TypeError(
|
|
60
|
+
`orpc-fn: MCP tool ${tool.name} needs an object input`
|
|
61
|
+
)
|
|
62
|
+
// MCP requires a top-level object; object unions retain their constraints.
|
|
63
|
+
return {
|
|
64
|
+
name: tool.name,
|
|
65
|
+
...tool.config,
|
|
66
|
+
inputSchema: { ...inputSchema, type: 'object' as const }
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
return {
|
|
70
|
+
names: [...tools.keys()] as readonly string[],
|
|
71
|
+
list: () => ({ tools: descriptions }),
|
|
72
|
+
call: async (
|
|
73
|
+
name: string,
|
|
74
|
+
args: Record<string, unknown> | undefined,
|
|
75
|
+
signal: AbortSignal
|
|
76
|
+
): Promise<TResult | TextToolResult> => {
|
|
77
|
+
const tool = tools.get(name)
|
|
78
|
+
if (!tool) return errorResult(`Unknown tool: ${name}`)
|
|
79
|
+
try {
|
|
80
|
+
signal.throwIfAborted()
|
|
81
|
+
const validation = await tool.config.inputSchema?.[
|
|
82
|
+
'~standard'
|
|
83
|
+
].validate(args ?? {})
|
|
84
|
+
if (validation?.issues)
|
|
85
|
+
return errorResult(
|
|
86
|
+
validation.issues.map((issue) => issue.message).join('; ')
|
|
87
|
+
)
|
|
88
|
+
const context = await options.context({ name: tool.name, signal })
|
|
89
|
+
const result = await call(tool.procedure, validation?.value, {
|
|
90
|
+
context,
|
|
91
|
+
signal
|
|
92
|
+
})
|
|
93
|
+
return await (options.formatResult ?? formatMcpResult)(result, tool)
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (signal.aborted) throw error
|
|
96
|
+
return errorResult(
|
|
97
|
+
error instanceof Error ? error.message : String(error)
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/mcp/sdk.ts
CHANGED
|
@@ -4,115 +4,33 @@ import {
|
|
|
4
4
|
ListToolsRequestSchema,
|
|
5
5
|
type CallToolResult
|
|
6
6
|
} from '@modelcontextprotocol/sdk/types.js'
|
|
7
|
+
import type { AnyRouter } from '@orpc/server'
|
|
7
8
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from '
|
|
12
|
-
import { isObjectJsonSchema } from '../json-schema.js'
|
|
13
|
-
import { listTools, type McpTool, type ProcedureFilter } from '../mcp.js'
|
|
9
|
+
createToolHandlers,
|
|
10
|
+
formatMcpResult,
|
|
11
|
+
type RegisterMcpToolsOptions as CoreOptions
|
|
12
|
+
} from './core.js'
|
|
14
13
|
|
|
15
|
-
export
|
|
16
|
-
/** Tool exposure must be an explicit application choice. */
|
|
17
|
-
filter: ProcedureFilter
|
|
18
|
-
context: (request: {
|
|
19
|
-
name: string
|
|
20
|
-
signal: AbortSignal
|
|
21
|
-
}) => InferRouterInitialContext<R> | Promise<InferRouterInitialContext<R>>
|
|
22
|
-
readOnly?: ProcedureFilter
|
|
23
|
-
name?: NonNullable<Parameters<typeof listTools>[1]>['name']
|
|
24
|
-
formatResult?: (
|
|
25
|
-
value: unknown,
|
|
26
|
-
tool: McpTool
|
|
27
|
-
) => CallToolResult | Promise<CallToolResult>
|
|
28
|
-
}
|
|
14
|
+
export { formatMcpResult }
|
|
29
15
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
? value
|
|
35
|
-
: (JSON.stringify(value, (_key, item) =>
|
|
36
|
-
typeof item === 'bigint' ? item.toString() : item
|
|
37
|
-
) ?? 'null')
|
|
38
|
-
return { content: [{ type: 'text', text }] }
|
|
39
|
-
}
|
|
16
|
+
export type RegisterMcpToolsOptions<R extends AnyRouter> = CoreOptions<
|
|
17
|
+
R,
|
|
18
|
+
CallToolResult
|
|
19
|
+
>
|
|
40
20
|
|
|
41
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Register on the SDK v1 low-level Server, or on McpServer.server. For SDK v2
|
|
23
|
+
* (`@modelcontextprotocol/server`) use `orpc-fn/mcp/server`.
|
|
24
|
+
*/
|
|
42
25
|
export function registerMcpTools<R extends AnyRouter>(
|
|
43
26
|
server: Pick<Server, 'setRequestHandler'>,
|
|
44
27
|
router: R,
|
|
45
28
|
options: RegisterMcpToolsOptions<R>
|
|
46
29
|
): readonly string[] {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const descriptions = definitions.map((tool) => {
|
|
54
|
-
const inputSchema = tool.config.inputSchema?.[
|
|
55
|
-
'~standard'
|
|
56
|
-
].jsonSchema.input() ?? { type: 'object', properties: {} }
|
|
57
|
-
if (!isObjectJsonSchema(inputSchema))
|
|
58
|
-
throw new TypeError(
|
|
59
|
-
`orpc-fn: MCP tool ${tool.name} needs an object input`
|
|
60
|
-
)
|
|
61
|
-
// MCP requires a top-level object; object unions retain their constraints.
|
|
62
|
-
return {
|
|
63
|
-
name: tool.name,
|
|
64
|
-
...tool.config,
|
|
65
|
-
inputSchema: { ...inputSchema, type: 'object' as const }
|
|
66
|
-
}
|
|
67
|
-
})
|
|
68
|
-
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
69
|
-
tools: descriptions
|
|
70
|
-
}))
|
|
71
|
-
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
72
|
-
const tool = tools.get(request.params.name)
|
|
73
|
-
if (!tool)
|
|
74
|
-
return {
|
|
75
|
-
content: [
|
|
76
|
-
{ type: 'text', text: `Unknown tool: ${request.params.name}` }
|
|
77
|
-
],
|
|
78
|
-
isError: true
|
|
79
|
-
}
|
|
80
|
-
try {
|
|
81
|
-
extra.signal.throwIfAborted()
|
|
82
|
-
const validation = await tool.config.inputSchema?.['~standard'].validate(
|
|
83
|
-
request.params.arguments ?? {}
|
|
84
|
-
)
|
|
85
|
-
if (validation?.issues)
|
|
86
|
-
return {
|
|
87
|
-
content: [
|
|
88
|
-
{
|
|
89
|
-
type: 'text',
|
|
90
|
-
text: validation.issues.map((issue) => issue.message).join('; ')
|
|
91
|
-
}
|
|
92
|
-
],
|
|
93
|
-
isError: true
|
|
94
|
-
}
|
|
95
|
-
const context = await options.context({
|
|
96
|
-
name: tool.name,
|
|
97
|
-
signal: extra.signal
|
|
98
|
-
})
|
|
99
|
-
const result = await call(tool.procedure, validation?.value, {
|
|
100
|
-
context,
|
|
101
|
-
signal: extra.signal
|
|
102
|
-
})
|
|
103
|
-
return await (options.formatResult ?? formatMcpResult)(result, tool)
|
|
104
|
-
} catch (error) {
|
|
105
|
-
if (extra.signal.aborted) throw error
|
|
106
|
-
return {
|
|
107
|
-
content: [
|
|
108
|
-
{
|
|
109
|
-
type: 'text',
|
|
110
|
-
text: error instanceof Error ? error.message : String(error)
|
|
111
|
-
}
|
|
112
|
-
],
|
|
113
|
-
isError: true
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
})
|
|
117
|
-
return [...tools.keys()]
|
|
30
|
+
const handlers = createToolHandlers(router, options)
|
|
31
|
+
server.setRequestHandler(ListToolsRequestSchema, handlers.list)
|
|
32
|
+
server.setRequestHandler(CallToolRequestSchema, (request, extra) =>
|
|
33
|
+
handlers.call(request.params.name, request.params.arguments, extra.signal)
|
|
34
|
+
)
|
|
35
|
+
return handlers.names
|
|
118
36
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CallToolResult, Server } from '@modelcontextprotocol/server'
|
|
2
|
+
import type { AnyRouter } from '@orpc/server'
|
|
3
|
+
import {
|
|
4
|
+
createToolHandlers,
|
|
5
|
+
formatMcpResult,
|
|
6
|
+
type RegisterMcpToolsOptions as CoreOptions
|
|
7
|
+
} from './core.js'
|
|
8
|
+
|
|
9
|
+
export { formatMcpResult }
|
|
10
|
+
|
|
11
|
+
export type RegisterMcpToolsOptions<R extends AnyRouter> = CoreOptions<
|
|
12
|
+
R,
|
|
13
|
+
CallToolResult
|
|
14
|
+
>
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Register on the SDK v2 (`@modelcontextprotocol/server`) low-level Server, or
|
|
18
|
+
* on McpServer.server. Declare `capabilities: { tools: {} }` on the Server.
|
|
19
|
+
*/
|
|
20
|
+
export function registerMcpTools<R extends AnyRouter>(
|
|
21
|
+
server: Pick<Server, 'setRequestHandler'>,
|
|
22
|
+
router: R,
|
|
23
|
+
options: RegisterMcpToolsOptions<R>
|
|
24
|
+
): readonly string[] {
|
|
25
|
+
const handlers = createToolHandlers(router, options)
|
|
26
|
+
server.setRequestHandler('tools/list', handlers.list)
|
|
27
|
+
server.setRequestHandler('tools/call', (request, ctx) =>
|
|
28
|
+
handlers.call(
|
|
29
|
+
request.params.name,
|
|
30
|
+
request.params.arguments,
|
|
31
|
+
ctx.mcpReq.signal
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
return handlers.names
|
|
35
|
+
}
|
package/src/otel.ts
CHANGED
|
@@ -195,6 +195,14 @@ export function errorMessageOf(error: unknown): string {
|
|
|
195
195
|
return error instanceof Error ? error.message : String(error)
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/** Error fields for a log line, named like the `fn.completed` ones. */
|
|
199
|
+
export function errorAttributes(error: unknown) {
|
|
200
|
+
return {
|
|
201
|
+
error_type: error instanceof Error ? error.name : typeof error,
|
|
202
|
+
error_message: errorMessageOf(error)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
198
206
|
function protectSpan<T extends SpanLike>(span: T): T {
|
|
199
207
|
return new Proxy(span, {
|
|
200
208
|
get(target, key) {
|
package/src/router.ts
CHANGED
|
@@ -51,3 +51,22 @@ export async function createStreamManifestAsync(
|
|
|
51
51
|
)
|
|
52
52
|
return paths
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Deterministic module source for a stream manifest: paths sorted, `json`
|
|
57
|
+
* as an array, `ts` as `export const <name>`. Used by `orpc-fn stream-manifest`.
|
|
58
|
+
*/
|
|
59
|
+
export function renderStreamManifest(
|
|
60
|
+
paths: readonly (readonly string[])[],
|
|
61
|
+
options: { format: 'json' | 'ts'; name?: string | undefined }
|
|
62
|
+
): string {
|
|
63
|
+
const lines = paths
|
|
64
|
+
.map(
|
|
65
|
+
(path) =>
|
|
66
|
+
`\t[${path.map((segment) => JSON.stringify(segment)).join(', ')}]`
|
|
67
|
+
)
|
|
68
|
+
.sort()
|
|
69
|
+
const array = lines.length ? `[\n${lines.join(',\n')}\n]` : '[]'
|
|
70
|
+
if (options.format === 'json') return `${array}\n`
|
|
71
|
+
return `// Generated by \`orpc-fn stream-manifest\`. Do not edit.\nexport const ${options.name ?? 'streamPaths'}: readonly (readonly string[])[] = ${array}\n`
|
|
72
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -140,6 +140,17 @@ export type RejectKeys<T, K extends PropertyKey, TMessage extends string> = {
|
|
|
140
140
|
[P in Extract<keyof T, K>]: TMessage
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/** Params every handler receives; extras may not reuse these names. */
|
|
144
|
+
type HandlerParamKey =
|
|
145
|
+
| 'input'
|
|
146
|
+
| 'context'
|
|
147
|
+
| 'call'
|
|
148
|
+
| 'signal'
|
|
149
|
+
| 'span'
|
|
150
|
+
| 'logger'
|
|
151
|
+
| 'errors'
|
|
152
|
+
| 'lastEventId'
|
|
153
|
+
|
|
143
154
|
/**
|
|
144
155
|
* Handler extras must have known keys: an index signature would claim every
|
|
145
156
|
* handler param. Intersected into `createFn`'s options to reject it.
|
|
@@ -149,17 +160,7 @@ export type FiniteExtras<T> = T extends unknown
|
|
|
149
160
|
? { extras: 'orpc-fn: extras need known keys, not an index signature' }
|
|
150
161
|
: number extends keyof T
|
|
151
162
|
? { extras: 'orpc-fn: extras need known keys, not an index signature' }
|
|
152
|
-
: Extract<
|
|
153
|
-
keyof T,
|
|
154
|
-
| 'input'
|
|
155
|
-
| 'context'
|
|
156
|
-
| 'call'
|
|
157
|
-
| 'signal'
|
|
158
|
-
| 'span'
|
|
159
|
-
| 'logger'
|
|
160
|
-
| 'errors'
|
|
161
|
-
| 'lastEventId'
|
|
162
|
-
> extends never
|
|
163
|
+
: Extract<keyof T, HandlerParamKey> extends never
|
|
163
164
|
? unknown
|
|
164
165
|
: { extras: 'orpc-fn: extras cannot shadow handler parameters' }
|
|
165
166
|
: never
|
|
@@ -272,14 +273,7 @@ export type HandlerParams<
|
|
|
272
273
|
: object
|
|
273
274
|
: object
|
|
274
275
|
>,
|
|
275
|
-
|
|
276
|
-
| 'context'
|
|
277
|
-
| 'call'
|
|
278
|
-
| 'signal'
|
|
279
|
-
| 'span'
|
|
280
|
-
| 'logger'
|
|
281
|
-
| 'errors'
|
|
282
|
-
| 'lastEventId'
|
|
276
|
+
HandlerParamKey
|
|
283
277
|
> & {
|
|
284
278
|
input: TInput
|
|
285
279
|
context: FnContext<TDef, TKey>
|
|
@@ -506,26 +500,14 @@ export type CompletedHookEvent<
|
|
|
506
500
|
)
|
|
507
501
|
}[keyof TProcedures & string]
|
|
508
502
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
| 'span'
|
|
521
|
-
| 'logger'
|
|
522
|
-
| 'errors'
|
|
523
|
-
| 'lastEventId'
|
|
524
|
-
> extends never
|
|
525
|
-
? never
|
|
526
|
-
: K
|
|
527
|
-
}[keyof T] extends never
|
|
528
|
-
? unknown
|
|
529
|
-
: {
|
|
530
|
-
extrasByProcedure: 'orpc-fn: scoped extras need finite keys and cannot shadow handler parameters'
|
|
531
|
-
}
|
|
503
|
+
/**
|
|
504
|
+
* Intersected into each scoped extras function's return type: rejects index
|
|
505
|
+
* signatures and handler-parameter names at the offending property. A
|
|
506
|
+
* top-level conditional over the whole map broke inference when zero-argument
|
|
507
|
+
* and context-reading functions were mixed.
|
|
508
|
+
*/
|
|
509
|
+
export type ScopedExtrasGuard<T> = (string extends keyof T
|
|
510
|
+
? { 'orpc-fn: scoped extras need finite keys': never }
|
|
511
|
+
: number extends keyof T
|
|
512
|
+
? { 'orpc-fn: scoped extras need finite keys': never }
|
|
513
|
+
: unknown) & { [K in HandlerParamKey]?: never }
|