@sylphx/mcp-server-sdk
v2.1.1
Published
Pure functional MCP server SDK for Node.js and Bun - type-safe, high performance
Readme
@sylphx/mcp-server-sdk
Pure functional MCP (Model Context Protocol) server SDK for Bun.
Features
- Pure Functional: Immutable data, composable handlers
- Type-Safe: First-class TypeScript with Vex schema integration
- Builder Pattern: Fluent API for defining tools, resources, and prompts
- Fast: Built for Bun with minimal dependencies
- Streamable HTTP: MCP 2025-03-26 spec with SSE notifications
- Complete: Tools, resources, prompts, notifications, sampling, elicitation
Installation
bun add @sylphx/mcp-server-sdkQuick Start
import { createServer, tool, text, stdio } from "@sylphx/mcp-server-sdk"
import { object, str } from "@sylphx/vex"
// Define tools using builder pattern
const greet = tool()
.description("Greet someone")
.input(object({ name: str() }))
.handler(({ input }) => text(`Hello, ${input.name}!`))
const ping = tool()
.handler(() => text("pong"))
// Create and start server
const server = createServer({
name: "my-server",
version: "1.0.0",
tools: { greet, ping },
transport: stdio()
})
await server.start()Tools
Tools are callable functions exposed to the AI.
import { tool, text, image, audio, json, toolError } from "@sylphx/mcp-server-sdk"
import { description, enum_, num, object, str } from "@sylphx/vex"
// Simple tool (no input)
const ping = tool()
.description("Health check")
.handler(() => text("pong"))
// Tool with typed input
const calculator = tool()
.description("Perform arithmetic")
.input(object({
a: num(description("First number")),
b: num(description("Second number")),
op: enum_(["+", "-", "*", "/"] as const),
}))
.handler(({ input }) => {
const { a, b, op } = input
const result = op === "+" ? a + b
: op === "-" ? a - b
: op === "*" ? a * b
: a / b
return text(`${a} ${op} ${b} = ${result}`)
})
// Multiple content items
const systemInfo = tool()
.description("Get system information")
.handler(() => [
text("CPU: 8 cores"),
text("Memory: 16GB")
])
// Mixed content types
const screenshot = tool()
.description("Take screenshot with description")
.handler(() => [
text("Here's the screenshot:"),
image(base64Data, "image/png")
])
// Return JSON data
const getUser = tool()
.description("Get user data")
.input(object({ id: str() }))
.handler(({ input }) => json({ id: input.id, name: "Alice" }))
// Return error
const riskyOperation = tool()
.description("May fail")
.handler(() => toolError("Something went wrong"))Resources
Resources provide data to the AI.
import { resource, resourceTemplate, resourceText, resourceBlob } from "@sylphx/mcp-server-sdk"
// Static resource with fixed URI
const readme = resource()
.uri("file:///readme.md")
.description("Project readme")
.mimeType("text/markdown")
.handler(({ uri }) => resourceText(uri, "# My Project\n\nWelcome!"))
// Resource template for dynamic URIs
const fileReader = resourceTemplate()
.uriTemplate("file:///{path}")
.description("Read any file")
.handler(async ({ uri, params }) => {
const content = await Bun.file(`/${params.path}`).text()
return resourceText(uri, content)
})
// Binary resource
const logo = resource()
.uri("image:///logo.png")
.mimeType("image/png")
.handler(async ({ uri }) => {
const data = await Bun.file("./logo.png").bytes()
const base64 = Buffer.from(data).toString("base64")
return resourceBlob(uri, base64, "image/png")
})Prompts
Prompts are reusable conversation templates.
import { prompt, user, assistant, messages, promptResult } from "@sylphx/mcp-server-sdk"
import { description, object, optional, str, withDefault } from "@sylphx/vex"
// Simple prompt (no arguments)
const greeting = prompt()
.description("A friendly greeting")
.handler(() => messages(
user("Hello!"),
assistant("Hi there! How can I help you today?")
))
// Prompt with typed arguments
const codeReview = prompt()
.description("Review code for issues")
.args(object({
code: str(description("Code to review")),
language: optional(str(description("Programming language"))),
}))
.handler(({ args }) => messages(
user(`Please review this ${args.language ?? "code"}:\n\`\`\`\n${args.code}\n\`\`\``),
assistant("I'll analyze this code for potential issues, best practices, and improvements.")
))
// Prompt with description in result
const translate = prompt()
.description("Translate text between languages")
.args(object({
text: str(),
from: withDefault(str(), "auto"),
to: str(),
}))
.handler(({ args }) => promptResult(
`Translation from ${args.from} to ${args.to}`,
messages(user(`Translate "${args.text}" from ${args.from} to ${args.to}`))
))Server Configuration
import { createServer, stdio, http } from "@sylphx/mcp-server-sdk"
const server = createServer({
// Server identity
name: "my-server",
version: "1.0.0",
instructions: "This server provides...",
// Handlers (names from object keys)
tools: { greet, ping, calculator },
resources: { readme, config },
resourceTemplates: { file: fileReader },
prompts: { codeReview, translate },
// Transport
transport: stdio() // or http({ port: 3000 })
})
await server.start()Transports
Stdio Transport
For CLI tools and subprocess communication.
import { stdio } from "@sylphx/mcp-server-sdk"
const server = createServer({
tools: { ping },
transport: stdio()
})
await server.start()HTTP Transport
Implements MCP Streamable HTTP (2025-03-26 spec) with SSE support for real-time notifications.
import { http } from "@sylphx/mcp-server-sdk"
const server = createServer({
tools: { ping },
transport: http({
port: 3000,
cors: "*" // Enable CORS for web clients
})
})
await server.start()
// Server running at http://localhost:3000/mcpEndpoints:
POST /mcp- JSON-RPC messages (returns JSON or SSE based on Accept header)GET /mcp/health- Health check
SSE Streaming:
When client sends Accept: text/event-stream, the server responds with SSE format, enabling real-time notifications during request processing:
event: message
data: {"jsonrpc":"2.0","method":"notifications/progress","params":{...}}
event: message
data: {"jsonrpc":"2.0","id":1,"result":{...}}Notifications
Send server-to-client notifications for progress and logging using the simplified context API.
import { array, object, str } from "@sylphx/vex"
const processFiles = tool()
.description("Process multiple files")
.input(object({ files: array(str()) }))
.handler(async ({ input, ctx }) => {
const total = input.files.length
for (let i = 0; i < total; i++) {
// Report progress (automatically uses progressToken from request)
ctx.progress(i + 1, { total, message: `Processing ${input.files[i]}` })
await processFile(input.files[i])
}
// Log completion
ctx.log("info", { message: "Processing complete" })
return text(`Processed ${total} files`)
})Context Methods
// Progress notification (uses progressToken from request automatically)
ctx.progress(current: number, options?: { total?: number; message?: string })
// Log notification
ctx.log(level: "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency", data: unknown, logger?: string)Low-level Notification Factories
For advanced use cases, you can use the raw notification factories:
import { progress, log, resourcesListChanged, toolsListChanged, promptsListChanged, resourceUpdated, cancelled } from "@sylphx/mcp-server-sdk"
// Progress notification (requires manual token)
progress(token: string | number, current: number, options?: { total?: number; message?: string })
// Log notification
log(level: LogLevel, data: unknown, logger?: string)
// List change notifications (for dynamic capability updates)
resourcesListChanged()
toolsListChanged()
promptsListChanged()
// Resource updated notification
resourceUpdated(uri: string)
// Cancellation notification
cancelled(requestId: string | number, reason?: string)Sampling
Request LLM completions from the client.
import { createSamplingClient } from "@sylphx/mcp-server-sdk"
import { object, str } from "@sylphx/vex"
const summarize = tool()
.description("Summarize text using AI")
.input(object({ text: str() }))
.handler(async ({ input, ctx }) => {
const sampling = createSamplingClient(ctx.requestSampling)
const result = await sampling.createMessage({
messages: [
{ role: "user", content: { type: "text", text: `Summarize: ${input.text}` } }
],
maxTokens: 500,
// Optional parameters
systemPrompt: "You are a helpful summarizer",
temperature: 0.7,
stopSequences: ["END"],
modelPreferences: {
hints: [{ name: "claude-3" }],
costPriority: 0.5,
speedPriority: 0.5,
intelligencePriority: 0.8,
},
})
// result.content is the response content
// result.model is the model used
// result.stopReason is why generation stopped
return text(result.content.text)
})Elicitation
Request user input from the client.
import { createElicitationClient } from "@sylphx/mcp-server-sdk"
import { object, str } from "@sylphx/vex"
const confirmAction = tool()
.description("Confirm before proceeding")
.input(object({ action: str() }))
.handler(async ({ input, ctx }) => {
const elicit = createElicitationClient(ctx.requestElicitation)
const result = await elicit.elicit(
`Are you sure you want to ${input.action}?`,
{
type: "object",
properties: {
confirm: {
type: "boolean",
description: "Confirm action",
},
reason: {
type: "string",
description: "Optional reason",
},
},
required: ["confirm"],
}
)
// result.action: "accept" | "decline" | "cancel"
// result.content: { confirm: boolean, reason?: string } (when action is "accept")
if (result.action === "accept" && result.content?.confirm) {
return text(`Proceeding with ${input.action}`)
}
return text("Action cancelled")
})Elicitation Schema Properties
interface ElicitationProperty {
type: "string" | "number" | "integer" | "boolean"
description?: string
default?: string | number | boolean
enum?: (string | number)[] // Constrain to specific values
enumNames?: string[] // Display names for enum values
// String-specific
format?: "email" | "uri" | "date" | "date-time"
minLength?: number
maxLength?: number
// Number-specific
minimum?: number
maximum?: number
}Pagination
Paginate large result sets.
import { paginate } from "@sylphx/mcp-server-sdk"
import { object, optional, str } from "@sylphx/vex"
const listItems = tool()
.description("List items with pagination")
.input(object({ cursor: optional(str()) }))
.handler(async ({ input }) => {
const allItems = await fetchAllItems()
const result = paginate(allItems, input.cursor, {
defaultPageSize: 10,
maxPageSize: 100,
})
// result.items: current page items
// result.nextCursor: cursor for next page (undefined if last page)
return json({
items: result.items,
nextCursor: result.nextCursor,
})
})API Reference
Server
createServer(config: ServerConfig): Server
interface ServerConfig {
name?: string // Default: "mcp-server"
version?: string // Default: "1.0.0"
instructions?: string // Instructions for the LLM
tools?: Record<string, ToolDefinition>
resources?: Record<string, ResourceDefinition>
resourceTemplates?: Record<string, ResourceTemplateDefinition>
prompts?: Record<string, PromptDefinition>
transport: TransportFactory
}Tool Builder
tool()
.description(string) // Optional description
.input(VexSchema) // Optional input schema
.handler(fn: HandlerFn) -> ToolDefinition
// Handler signature
({ input, ctx }) => ToolResult | Promise<ToolResult>
// Handler can return:
// - Single content: text("hello")
// - Array: [text("hi"), image(data, "image/png")]
// - Full result: { content: [...], isError: true }Resource Builder
resource()
.uri(string) // Required URI
.description(string) // Optional description
.mimeType(string) // Optional MIME type
.handler(fn) -> ResourceDefinition
resourceTemplate()
.uriTemplate(string) // Required URI template (RFC 6570)
.description(string) // Optional description
.mimeType(string) // Optional MIME type
.handler(fn) -> ResourceTemplateDefinition
// Handler receives { uri, ctx } or { uri, params, ctx }Prompt Builder
prompt()
.description(string) // Optional description
.args(VexSchema) // Optional arguments schema
.handler(fn) -> PromptDefinition
// Handler receives { args, ctx } or { ctx }Content Helpers
// Tool content
text(content: string, annotations?): TextContent
image(data: string, mimeType: string, annotations?): ImageContent
audio(data: string, mimeType: string, annotations?): AudioContent
embedded(resource: EmbeddedResource, annotations?): ResourceContent
json(data: unknown): TextContent
toolError(message: string): ToolsCallResult
// Resources
resourceText(uri: string, text: string, mimeType?: string): ResourcesReadResult
resourceBlob(uri: string, blob: string, mimeType: string): ResourcesReadResult
resourceContents(...items: EmbeddedResource[]): ResourcesReadResult
// Prompts
user(content: string): PromptMessage
assistant(content: string): PromptMessage
messages(...msgs: PromptMessage[]): PromptsGetResult
promptResult(description: string, result: PromptsGetResult): PromptsGetResultTransports
stdio(options?: StdioOptions): TransportFactory
http(options?: HttpOptions): TransportFactory
interface HttpOptions {
port?: number // Default: 3000
hostname?: string // Default: "localhost"
}Notifications
progress(token, current, options?): ProgressNotification
log(level, data, logger?): LogNotification
resourcesListChanged(): Notification
toolsListChanged(): Notification
promptsListChanged(): Notification
resourceUpdated(uri): Notification
cancelled(requestId, reason?): NotificationSampling
createSamplingClient(sender: SamplingRequestSender): SamplingClient
interface SamplingClient {
createMessage(params: SamplingCreateParams): Promise<SamplingCreateResult>
}Elicitation
createElicitationClient(sender: ElicitationRequestSender): ElicitationClient
interface ElicitationClient {
elicit(message: string, schema: ElicitationSchema): Promise<ElicitationCreateResult>
}Pagination
paginate<T>(items: T[], cursor?: string, options?: PaginationOptions): PageResult<T>
interface PaginationOptions {
defaultPageSize?: number // Default: 50
maxPageSize?: number // Default: 100
}
interface PageResult<T> {
items: T[]
nextCursor?: string
}Powered by Sylphx
- @sylphx/vex - Ultra-fast schema validation
- @sylphx/gust - Lightweight HTTP framework for Bun
- @sylphx/biome-config - Shared Biome configuration
- @sylphx/tsconfig - Shared TypeScript configuration
- @sylphx/doctor - Project health checker
- @sylphx/bump - Version management
License
MIT
