npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@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.

npm MCP Conformance

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-sdk

Quick 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/mcp

Endpoints:

  • 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): PromptsGetResult

Transports

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?): Notification

Sampling

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

License

MIT