1/**
2 * Version-neutral MCP server records and semantic operations.
3 *
4 * @internal
5 */
6
7import * as Arr from "../../Array.ts"
8import * as Data from "../../Data.ts"
9import * as Effect from "../../Effect.ts"
10import type * as Schema from "../../Schema.ts"
11import type * as McpProtocol from "../McpProtocol.ts"
12import * as McpSchema from "../McpSchema.ts"
13
14/**
15 * @internal
16 */
17export interface InputRequiredFields {
18 readonly inputRequests?: Readonly<Record<string, McpSchema.McpInputRequest>> | undefined
19 readonly requestState?: string | undefined
20}
21
22/**
23 * @internal
24 */
25export type OperationOutcome<A> = Data.TaggedEnum<{
26 Complete: { readonly value: A }
27 InputRequired: InputRequiredFields
28}>
29
30/**
31 * @internal
32 */
33export const OperationOutcome = {
34 Complete: <A>(value: A): OperationOutcome<A> => ({ _tag: "Complete", value }),
35 InputRequired: (fields: InputRequiredFields): OperationOutcome<never> => ({
36 _tag: "InputRequired",
37 ...fields
38 })
39}
40
41/**
42 * @internal
43 */
44export type CanonicalRequestMetadata = NonNullable<
45 typeof McpSchema.Initialize.payloadSchema.Type["_meta"]
46>
47
48/**
49 * @internal
50 */
51export interface NegotiatedProtocolProfile<out Version extends string = McpProtocol.StatefulProtocolVersion> {
52 // Core decisions receive negotiated facts rather than dated wire requests.
53 readonly protocolVersion: Version
54 readonly clientCapabilities: McpSchema.ClientCapabilities
55 readonly clientInfo: Version extends McpProtocol.StatefulProtocolVersion ? McpSchema.Implementation
56 : McpSchema.Implementation | undefined
57 readonly requestMetadata?:
58 | (Version extends McpProtocol.StatefulProtocolVersion ? CanonicalRequestMetadata
59 : CanonicalRequestMetadata | Schema.JsonObject)
60 | undefined
61}
62
63// NOTE: Capabilities remain a normalized core model because dated revisions
64// advertise different fields and encode capability presence differently.
65/**
66 * @internal
67 */
68export interface CanonicalServerCapabilities {
69 readonly experimental?: Readonly<Record<string, Schema.JsonObject>> | undefined
70 readonly logging?: boolean | undefined
71 readonly completions?: boolean | undefined
72 readonly prompts?: Readonly<{ readonly listChanged?: boolean | undefined }> | undefined
73 readonly resources?:
74 | Readonly<{
75 readonly subscribe?: boolean | undefined
76 readonly listChanged?: boolean | undefined
77 }>
78 | undefined
79 readonly tools?: Readonly<{ readonly listChanged?: boolean | undefined }> | undefined
80 readonly extensions?: Schema.JsonObject | undefined
81}
82
83/**
84 * @internal
85 */
86export interface CanonicalInitializeResult {
87 readonly capabilities: CanonicalServerCapabilities
88 readonly serverInfo: McpSchema.Implementation
89 readonly instructions?: string | undefined
90}
91
92/**
93 * @internal
94 */
95export interface McpInvocation {
96 readonly clientId: number
97 readonly protocol: NegotiatedProtocolProfile<string>
98 readonly requestContext: McpSchema.McpRequestContext["Service"]
99 readonly serverClient?: McpSchema.McpServerClient["Service"] | undefined
100}
101
102// NOTE: McpInvocation is runtime context, not a wire DTO. It combines the
103// negotiated profile with the request-scoped service used by handlers.
104
105/**
106 * @internal
107 */
108export class ResourceNotFound extends Data.TaggedError("ResourceNotFound")<{
109 readonly uri: string
110}> {}
111
112// NOTE: Core errors preserve semantic failure categories before adapters map
113// them to dated MCP error codes and messages.
114/**
115 * @internal
116 */
117export class ToolNotFound extends Data.TaggedError("ToolNotFound")<{
118 readonly name: string
119}> {}
120
121/**
122 * @internal
123 */
124export class InvalidToolInput extends Data.TaggedError("InvalidToolInput")<{
125 readonly name: string
126 readonly message: string
127}> {}
128
129/**
130 * @internal
131 */
132export class InvalidToolContinuation extends Data.TaggedError("InvalidToolContinuation")<{
133 readonly name: string
134 readonly message: string
135}> {}
136
137/**
138 * @internal
139 */
140export class ToolExecutionError extends Data.TaggedError("ToolExecutionError")<{
141 readonly name: string
142 readonly message: string
143}> {}
144
145/**
146 * @internal
147 */
148export class UnsupportedByProtocol extends Data.TaggedError("UnsupportedByProtocol")<{
149 readonly protocolVersion: McpProtocol.ProtocolVersion
150 readonly feature: string
151}> {}
152
153/**
154 * @internal
155 */
156export type ToolError =
157 | ToolNotFound
158 | InvalidToolInput
159 | InvalidToolContinuation
160 | ToolExecutionError
161
162/**
163 * @internal
164 */
165export interface ToolRegistration {
166 // The canonical Tool copy normalizes its top-level title from
167 // `tool.title ?? tool.annotations?.title` at the public boundary.
168 readonly descriptor: McpSchema.Tool
169 readonly isVisible: (profile: NegotiatedProtocolProfile<string>) => boolean
170 readonly handle: (
171 call: typeof McpSchema.CallTool.payloadSchema.Type,
172 invocation: McpInvocation
173 ) => Effect.Effect<
174 OperationOutcome<McpSchema.CallToolResult>,
175 InvalidToolInput | InvalidToolContinuation | ToolExecutionError,
176 never
177 >
178}
179
180/**
181 * @internal
182 */
183export interface Tools {
184 readonly register: (
185 registration: ToolRegistration
186 ) => Effect.Effect<void>
187 readonly list: (
188 profile: NegotiatedProtocolProfile<string>
189 ) => Effect.Effect<ReadonlyArray<McpSchema.Tool>>
190 readonly call: (
191 call: typeof McpSchema.CallTool.payloadSchema.Type,
192 invocation: McpInvocation
193 ) => Effect.Effect<OperationOutcome<McpSchema.CallToolResult>, ToolError>
194}
195
196/**
197 * @internal
198 */
199export interface ResourceRegistration {
200 readonly descriptor: McpSchema.Resource
201 readonly isVisible: (profile: NegotiatedProtocolProfile<string>) => boolean
202 readonly read: (
203 invocation: McpInvocation
204 ) => Effect.Effect<McpSchema.ReadResourceResult, McpSchema.InternalError>
205}
206
207/**
208 * @internal
209 */
210export interface ResourceTemplateRegistration {
211 readonly descriptor: McpSchema.ResourceTemplate
212 readonly isVisible: (profile: NegotiatedProtocolProfile<string>) => boolean
213 readonly match: (uri: string) => ReadonlyArray<string> | undefined
214 readonly read: (
215 uri: string,
216 params: ReadonlyArray<string>,
217 invocation: McpInvocation
218 ) => Effect.Effect<McpSchema.ReadResourceResult, McpSchema.InvalidParams | McpSchema.InternalError>
219}
220
221/**
222 * @internal
223 */
224export interface Resources {
225 readonly register: (registration: ResourceRegistration) => Effect.Effect<void>
226 readonly registerTemplate: (registration: ResourceTemplateRegistration) => Effect.Effect<void>
227 readonly list: (
228 profile: NegotiatedProtocolProfile<string>
229 ) => Effect.Effect<ReadonlyArray<McpSchema.Resource>>
230 readonly listTemplates: (
231 profile: NegotiatedProtocolProfile<string>
232 ) => Effect.Effect<ReadonlyArray<McpSchema.ResourceTemplate>>
233 readonly read: (
234 uri: string,
235 invocation: McpInvocation
236 ) => Effect.Effect<
237 McpSchema.ReadResourceResult,
238 ResourceNotFound | McpSchema.InvalidParams | McpSchema.InternalError
239 >
240}
241
242/**
243 * @internal
244 */
245export class PromptNotFound extends Data.TaggedError("PromptNotFound")<{
246 readonly name: string
247}> {}
248
249/**
250 * @internal
251 */
252export interface PromptRegistration {
253 readonly descriptor: McpSchema.Prompt
254 readonly isVisible: (profile: NegotiatedProtocolProfile<string>) => boolean
255 readonly get: (
256 args: Readonly<Record<string, string>>,
257 invocation: McpInvocation
258 ) => Effect.Effect<OperationOutcome<McpSchema.GetPromptResult>, McpSchema.InvalidParams | McpSchema.InternalError>
259}
260
261/**
262 * @internal
263 */
264export interface Prompts {
265 readonly register: (registration: PromptRegistration) => Effect.Effect<void>
266 readonly list: (
267 profile: NegotiatedProtocolProfile<string>
268 ) => Effect.Effect<ReadonlyArray<McpSchema.Prompt>>
269 readonly get: (
270 name: string,
271 args: Readonly<Record<string, string>>,
272 invocation: McpInvocation
273 ) => Effect.Effect<
274 OperationOutcome<McpSchema.GetPromptResult>,
275 PromptNotFound | McpSchema.InvalidParams | McpSchema.InternalError
276 >
277}
278
279/**
280 * @internal
281 */
282export type CompletionReference =
283 | { readonly type: "prompt"; readonly name: string; readonly title?: string | undefined }
284 | { readonly type: "resourceTemplate"; readonly uriTemplate: string }
285
286// NOTE: Completion requests/results normalize dated reference tags, optional
287// context, and nested CompleteResult values.
288
289/**
290 * @internal
291 */
292export interface CompletionRequest {
293 readonly reference: CompletionReference
294 readonly argument: Readonly<{ readonly name: string; readonly value: string }>
295 readonly context?: Readonly<{ readonly arguments?: Readonly<Record<string, string>> }> | undefined
296 readonly metadata?: CanonicalRequestMetadata | undefined
297}
298
299/**
300 * @internal
301 */
302export interface CompletionResult {
303 readonly values: ReadonlyArray<string>
304 readonly total?: number | undefined
305 readonly hasMore?: boolean | undefined
306 readonly metadata?: Schema.JsonObject | undefined
307}
308
309/**
310 * @internal
311 */
312export interface Completions {
313 readonly register: (
314 key: string,
315 complete: (
316 request: CompletionRequest,
317 invocation: McpInvocation
318 ) => Effect.Effect<CompletionResult, McpSchema.InvalidParams | McpSchema.InternalError>
319 ) => Effect.Effect<void>
320 readonly complete: (
321 request: CompletionRequest,
322 invocation: McpInvocation
323 ) => Effect.Effect<CompletionResult, McpSchema.InvalidParams | McpSchema.InternalError>
324}
325
326/**
327 * @internal
328 */
329export type ClientNotification = Data.TaggedEnum<{
330 Initialized: {}
331 Progress: {
332 readonly progressToken: string | number
333 readonly progress: number
334 readonly total?: number | undefined
335 readonly message?: string | undefined
336 readonly metadata?: Schema.JsonObject | undefined
337 }
338 RootsChanged: {}
339}>
340
341/**
342 * @internal
343 */
344export const ClientNotification = Data.taggedEnum<ClientNotification>()
345
346/**
347 * @internal
348 */
349export interface Cancellation {
350 readonly requestId: string | number
351 readonly reason?: string | undefined
352 readonly metadata?: Schema.JsonObject | undefined
353}
354
355/**
356 * @internal
357 */
358export type ServerNotification = Data.TaggedEnum<{
359 Cancelled: {
360 readonly requestId: string | number
361 readonly reason?: string | undefined
362 readonly metadata?: Schema.JsonObject | undefined
363 }
364 Progress: {
365 readonly progressToken: string | number
366 readonly progress: number
367 readonly total?: number | undefined
368 readonly message?: string | undefined
369 readonly metadata?: Schema.JsonObject | undefined
370 }
371 LoggingMessage: {
372 readonly level: McpSchema.LoggingLevel
373 readonly logger?: string | undefined
374 readonly data: unknown
375 readonly metadata?: Schema.JsonObject | undefined
376 }
377 ResourceUpdated: { readonly uri: string; readonly metadata?: Schema.JsonObject | undefined }
378 ResourcesChanged: { readonly metadata?: Schema.JsonObject | undefined }
379 ToolsChanged: { readonly metadata?: Schema.JsonObject | undefined }
380 PromptsChanged: { readonly metadata?: Schema.JsonObject | undefined }
381 ElicitationComplete: { readonly elicitationId: string }
382}>
383
384/**
385 * @internal
386 */
387export const ServerNotification = Data.taggedEnum<ServerNotification>()
388
389/**
390 * @internal
391 */
392export interface McpCore {
393 readonly tools: Tools
394 readonly resources: Resources
395 readonly prompts: Prompts
396 readonly completions: Completions
397 readonly registrationPresence: Effect.Effect<{
398 readonly tools: boolean
399 readonly resources: boolean
400 readonly prompts: boolean
401 }>
402}
403
404/**
405 * @internal
406 */
407export const make: Effect.Effect<McpCore> = Effect.sync(() => {
408 const registrations = new Map<string, ToolRegistration>()
409 const resourceRegistrations: Array<ResourceRegistration> = []
410 const resourceTemplateRegistrations: Array<ResourceTemplateRegistration> = []
411 const promptRegistrations = new Map<string, PromptRegistration>()
412 const completionRegistrations = new Map<
413 string,
414 (
415 request: CompletionRequest,
416 invocation: McpInvocation
417 ) => Effect.Effect<CompletionResult, McpSchema.InvalidParams | McpSchema.InternalError>
418 >()
419
420 const tools: Tools = {
421 register: (registration) =>
422 Effect.sync(() => {
423 registrations.set(registration.descriptor.name, registration)
424 }),
425 list: (profile) =>
426 Effect.sync(() => {
427 const descriptors: Array<McpSchema.Tool> = []
428 for (const registration of registrations.values()) {
429 if (registration.isVisible(profile)) {
430 descriptors.push(registration.descriptor)
431 }
432 }
433 return descriptors
434 }),
435 call: (call, invocation) =>
436 Effect.suspend((): Effect.Effect<OperationOutcome<McpSchema.CallToolResult>, ToolError> => {
437 const registration = registrations.get(call.name)
438 if (registration === undefined) {
439 return new ToolNotFound({ name: call.name })
440 }
441 if (!registration.isVisible(invocation.protocol)) {
442 return new ToolNotFound({ name: call.name })
443 }
444 return registration.handle(call, invocation)
445 })
446 }
447
448 const resources: Resources = {
449 register: (registration) =>
450 Effect.sync(() => {
451 const index = resourceRegistrations.findIndex((entry) => entry.descriptor.uri === registration.descriptor.uri)
452 if (index === -1) {
453 resourceRegistrations.push(registration)
454 } else {
455 resourceRegistrations[index] = registration
456 }
457 }),
458 registerTemplate: (registration) =>
459 Effect.sync(() => {
460 const index = resourceTemplateRegistrations.findIndex(
461 (entry) => entry.descriptor.uriTemplate === registration.descriptor.uriTemplate
462 )
463 if (index === -1) {
464 resourceTemplateRegistrations.push(registration)
465 } else {
466 resourceTemplateRegistrations[index] = registration
467 }
468 }),
469 list: (profile) =>
470 Effect.sync(() =>
471 resourceRegistrations
472 .filter((entry) => entry.isVisible(profile))
473 .map((entry) => entry.descriptor)
474 ),
475 listTemplates: (profile) =>
476 Effect.sync(() =>
477 resourceTemplateRegistrations
478 .filter((entry) => entry.isVisible(profile))
479 .map((entry) => entry.descriptor)
480 ),
481 read: Effect.fnUntraced(function*(uri, invocation) {
482 const resource = resourceRegistrations.find((entry) => entry.descriptor.uri === uri)
483 if (resource !== undefined && resource.isVisible(invocation.protocol)) {
484 return yield* resource.read(invocation)
485 }
486 for (const template of resourceTemplateRegistrations) {
487 const params = template.match(uri)
488 if (params !== undefined && template.isVisible(invocation.protocol)) {
489 return yield* template.read(uri, params, invocation)
490 }
491 }
492 return yield* new ResourceNotFound({ uri })
493 })
494 }
495
496 const prompts: Prompts = {
497 register: (registration) =>
498 Effect.sync(() => {
499 promptRegistrations.set(registration.descriptor.name, registration)
500 }),
501 list: (profile) =>
502 Effect.sync(() =>
503 Array.from(promptRegistrations.values())
504 .filter((entry) => entry.isVisible(profile))
505 .map((entry) => entry.descriptor)
506 ),
507 get: Effect.fnUntraced(function*(name, args, invocation) {
508 const registration = promptRegistrations.get(name)
509 if (registration === undefined || !registration.isVisible(invocation.protocol)) {
510 return yield* new PromptNotFound({ name })
511 }
512 return yield* registration.get(args, invocation)
513 })
514 }
515
516 const completions: Completions = {
517 register: (key, complete) =>
518 Effect.sync(() => {
519 completionRegistrations.set(key, complete)
520 }),
521 complete: Effect.fnUntraced(function*(request, invocation) {
522 const key = request.reference.type === "prompt"
523 ? `prompt/${request.reference.name}/${request.argument.name}`
524 : `resource/${request.reference.uriTemplate}/${request.argument.name}`
525 const complete = completionRegistrations.get(key)
526 if (complete === undefined) {
527 return yield* new McpSchema.InvalidParams({ message: "Unknown completion reference or argument" })
528 }
529 const result = yield* complete(request, invocation)
530 const values = Arr.take(result.values, 100)
531 return {
532 ...result,
533 values,
534 hasMore: result.hasMore === true || values.length < result.values.length
535 }
536 })
537 }
538
539 const registrationPresence = Effect.sync(() => ({
540 tools: registrations.size > 0,
541 resources: resourceRegistrations.length > 0 || resourceTemplateRegistrations.length > 0,
542 prompts: promptRegistrations.size > 0
543 }))
544
545 return { tools, resources, prompts, completions, registrationPresence }
546})