Skip to content

Commit aa3a819

Browse files
IMax153tim-smart
andauthored
Introduce a native McpServer implementation for Effect (#4961)
Co-authored-by: Tim <[email protected]>
1 parent f891d45 commit aa3a819

File tree

17 files changed

+3289
-67
lines changed

17 files changed

+3289
-67
lines changed

.changeset/free-numbers-guess.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
"@effect/ai": patch
3+
---
4+
5+
add McpServer module
6+
7+
The McpServer module provides a way to implement a MCP server using Effect.
8+
9+
Here's an example of how to use the McpServer module to create a simple MCP
10+
server with a resource template and a test prompt:
11+
12+
```ts
13+
import { McpSchema, McpServer } from "@effect/ai"
14+
import { NodeRuntime, NodeSink, NodeStream } from "@effect/platform-node"
15+
import { Effect, Layer, Logger, Schema } from "effect"
16+
17+
const idParam = McpSchema.param("id", Schema.NumberFromString)
18+
19+
// Define a resource template for a README file
20+
const ReadmeTemplate = McpServer.resource`file://readme/${idParam}`({
21+
name: "README Template",
22+
// You can add auto-completion for the ID parameter
23+
completion: {
24+
id: (_) => Effect.succeed([1, 2, 3, 4, 5])
25+
},
26+
content: Effect.fn(function* (_uri, id) {
27+
return `# MCP Server Demo - ID: ${id}`
28+
})
29+
})
30+
31+
// Define a test prompt with parameters
32+
const TestPrompt = McpServer.prompt({
33+
name: "Test Prompt",
34+
description: "A test prompt to demonstrate MCP server capabilities",
35+
parameters: Schema.Struct({
36+
flightNumber: Schema.String
37+
}),
38+
completion: {
39+
flightNumber: () => Effect.succeed(["FL123", "FL456", "FL789"])
40+
},
41+
content: ({ flightNumber }) =>
42+
Effect.succeed(`Get the booking details for flight number: ${flightNumber}`)
43+
})
44+
45+
// Merge all the resources and prompts into a single server layer
46+
const ServerLayer = Layer.mergeAll(ReadmeTemplate, TestPrompt).pipe(
47+
// Provide the MCP server implementation
48+
Layer.provide(
49+
McpServer.layerStdio({
50+
name: "Demo Server",
51+
version: "1.0.0",
52+
stdin: NodeStream.stdin,
53+
stdout: NodeSink.stdout
54+
})
55+
),
56+
// add a stderr logger
57+
Layer.provide(Logger.add(Logger.prettyLogger({ stderr: true })))
58+
)
59+
60+
Layer.launch(ServerLayer).pipe(NodeRuntime.runMain)
61+
```

packages/ai/ai/docgen.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
"effect/*": ["../../../../effect/src/*.js"],
1616
"@effect/platform": ["../../../../platform/src/index.js"],
1717
"@effect/platform/*": ["../../../../platform/src/*.js"],
18+
"@effect/platform-node": ["../../../../platform-node/src/index.js"],
19+
"@effect/platform-node/*": ["../../../../platform-node/src/*.js"],
1820
"@effect/ai": ["../../../ai/src/index.js"],
1921
"@effect/ai/*": ["../../../ai/src/*.js"],
2022
"@effect/ai-openai": ["../../../ai-openai/src/index.js"],

packages/ai/ai/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,19 @@
4545
"test": "vitest",
4646
"coverage": "vitest --coverage"
4747
},
48-
"dependencies": {},
4948
"peerDependencies": {
5049
"@effect/experimental": "workspace:^",
5150
"@effect/platform": "workspace:^",
51+
"@effect/rpc": "workspace:^",
5252
"effect": "workspace:^"
5353
},
5454
"devDependencies": {
5555
"@effect/experimental": "workspace:^",
5656
"@effect/platform": "workspace:^",
57+
"@effect/rpc": "workspace:^",
5758
"effect": "workspace:^"
59+
},
60+
"dependencies": {
61+
"find-my-way-ts": "^0.1.5"
5862
}
5963
}

packages/ai/ai/src/AiTool.ts

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
/**
22
* @since 1.0.0
33
*/
4-
import type * as Context_ from "effect/Context"
4+
import * as Context_ from "effect/Context"
55
import type * as Effect from "effect/Effect"
6+
import { constFalse, constTrue } from "effect/Function"
67
import * as Option from "effect/Option"
78
import { type Pipeable, pipeArguments } from "effect/Pipeable"
89
import * as Predicate from "effect/Predicate"
@@ -75,6 +76,8 @@ export interface AiTool<
7576
*/
7677
readonly failureSchema: Failure
7778

79+
readonly annotations: Context_.Context<never>
80+
7881
/**
7982
* Adds a requirement on a particular service for the tool call to be able to
8083
* be executed.
@@ -88,7 +91,8 @@ export interface AiTool<
8891
Name,
8992
Parameters,
9093
SuccessSchema,
91-
Failure
94+
Failure,
95+
Requirements
9296
>
9397

9498
/**
@@ -98,7 +102,8 @@ export interface AiTool<
98102
Name,
99103
Parameters,
100104
Success,
101-
FailureSchema
105+
FailureSchema,
106+
Requirements
102107
>
103108

104109
/**
@@ -111,7 +116,31 @@ export interface AiTool<
111116
ParametersSchema extends Schema.Struct<infer _> ? ParametersSchema
112117
: ParametersSchema extends Schema.Struct.Fields ? Schema.Struct<ParametersSchema>
113118
: never,
114-
Success
119+
Success,
120+
Failure,
121+
Requirements
122+
>
123+
124+
/**
125+
* Add an annotation to the tool.
126+
*/
127+
annotate<I, S>(tag: Context_.Tag<I, S>, value: S): AiTool<
128+
Name,
129+
Parameters,
130+
Success,
131+
Failure,
132+
Requirements
133+
>
134+
135+
/**
136+
* Add many annotations to the tool.
137+
*/
138+
annotateContext<I>(context: Context_.Context<I>): AiTool<
139+
Name,
140+
Parameters,
141+
Success,
142+
Failure,
143+
Requirements
115144
>
116145
}
117146

@@ -133,6 +162,7 @@ export interface Any extends Pipeable {
133162
readonly description?: string | undefined
134163
readonly key: string
135164
readonly parametersSchema: AnyStructSchema
165+
readonly annotations: Context_.Context<never>
136166
}
137167

138168
/**
@@ -410,6 +440,18 @@ const Proto = {
410440
? parametersSchema as any
411441
: Schema.Struct(parametersSchema as any)
412442
})
443+
},
444+
annotate<I, S>(this: AnyWithProtocol, tag: Context_.Tag<I, S>, value: S) {
445+
return makeProto({
446+
...this,
447+
annotations: Context_.add(this.annotations, tag, value)
448+
})
449+
},
450+
annotateContext<I>(this: AnyWithProtocol, context: Context_.Context<I>) {
451+
return makeProto({
452+
...this,
453+
annotations: Context_.merge(this.annotations, context)
454+
})
413455
}
414456
}
415457

@@ -424,6 +466,7 @@ const makeProto = <
424466
readonly parametersSchema: Parameters
425467
readonly successSchema: Success
426468
readonly failureSchema: Failure
469+
readonly annotations: Context_.Context<never>
427470
}): AiTool<Name, Parameters, Success> => {
428471
const self = Object.assign(Object.create(Proto), options)
429472
self.key = `@effect/ai/AiTool/${options.name}`
@@ -474,7 +517,8 @@ export const make = <
474517
? Schema.Struct(options?.parameters as any)
475518
: constEmptyStruct,
476519
successSchema,
477-
failureSchema
520+
failureSchema,
521+
annotations: Context_.empty()
478522
}) as any
479523
}
480524

@@ -492,5 +536,44 @@ export const fromTaggedRequest = <S extends AnyTaggedRequestSchema>(
492536
description: Option.getOrUndefined(AST.getDescriptionAnnotation((schema.ast as any).to)),
493537
parametersSchema: schema as any,
494538
successSchema: schema.success as any,
495-
failureSchema: schema.failure as any
539+
failureSchema: schema.failure as any,
540+
annotations: Context_.empty()
496541
})
542+
543+
/**
544+
* @since 1.0.0
545+
* @category Annotations
546+
*/
547+
export class Title extends Context_.Tag("@effect/ai/AiTool/Title")<Title, string>() {}
548+
549+
/**
550+
* @since 1.0.0
551+
* @category Annotations
552+
*/
553+
export class Readonly extends Context_.Reference<Readonly>()("@effect/ai/AiTool/Readonly", {
554+
defaultValue: constFalse
555+
}) {}
556+
557+
/**
558+
* @since 1.0.0
559+
* @category Annotations
560+
*/
561+
export class Destructive extends Context_.Reference<Destructive>()("@effect/ai/AiTool/Destructive", {
562+
defaultValue: constTrue
563+
}) {}
564+
565+
/**
566+
* @since 1.0.0
567+
* @category Annotations
568+
*/
569+
export class Idempotent extends Context_.Reference<Idempotent>()("@effect/ai/AiTool/Idempotent", {
570+
defaultValue: constFalse
571+
}) {}
572+
573+
/**
574+
* @since 1.0.0
575+
* @category Annotations
576+
*/
577+
export class OpenWorld extends Context_.Reference<OpenWorld>()("@effect/ai/AiTool/OpenWorld", {
578+
defaultValue: constTrue
579+
}) {}

0 commit comments

Comments
 (0)