aide

Build Model Context Protocol (MCP) Servers (clients coming soon).

Package Version Hex Docs

Install

gleam add aide

Usage

Build a Server

This guide explains building an MCP server with some simple maths tools. See examples/math_server for the completed code.

Aide only supports building remote servers, not a problem as these can be used locally by directing your client to localhost:<port>. From this point when we say MCP Server we implicitly mean Remote Server.

1. Mounting an MCP server

An MCP server is a a single endpoint that accepts a JSON-RPC request and returns a JSON-RPC response. This endpoint can be hosted on it’s own or as part of a larger application. Any choice of web server is valid. This guide assume a wisp server.

A common approah to routing in wisp is to have a router module that pattern matches on path and method. The route serving mcp is /mcp, not it needs to handle all methods.

// math_server/www/router
import gleam/http
import gleam/http/request.{Request}
import math_server/www/mcp
import wisp

pub fn handle(request, context) {
  use <- wisp.log_request(request)
  let Request(method:, ..) = request
  case wisp.path_segments(request), method {
    [], http.Get -> wisp.html_response("Check out our MCP server", 200)

    ["mcp"], _any -> mcp.handle(request, context)
    _, _ -> wisp.not_found()
  }
}

2. Handle JSON RPC

The MCP protocol builds on JSON RPC, again the aide library lets you choose your own approach to JSON decoding.

The handle function in the math_server/www/mcp module encapsulates decoding to MCP specific types, including decoding your applications specific tool types.

The handling of the mcp requests is another handle function defined in the module math_server/mcp. Separating math_server/www/mcp and math_server/mcp allows for the same server to be used with other transports.

// math_server/www/mcp
import aide
import aide/definitions
import gleam/dynamic/decode
import gleam/http
import gleam/http/request.{Request}
import gleam/json
import gleam/option.{Some}
import gleam/string
import math_server/mcp
import wisp

pub fn handle(request, context) {
  let Request(method:, ..) = request
  case method {
    http.Delete -> wisp.no_content()
    http.Post -> {
      // decode input to a MCP request
      use mcp_request <- decode_json(request, aide.request_decoder())

      // create an mcp server config.
      // In this example the server is the same for all users, but if different tools are required for different users,
      // this is where you would create a user specific server.
      let server = server(context)

      // handle mcp request in the mcp module
      mcp.handle(mcp_request, server)
      // encode the response and return it
      |> option.map(aide.response_encode)
      |> option.map(json.to_string)
      |> option.map(wisp.json_response(_, 200))
      |> option.unwrap(wisp.response(202))
    }
    _ -> wisp.method_not_allowed([http.Post, http.Delete])
  }
}

fn decode_json(request, decoder, then) {
  use data <- wisp.require_json(request)
  case decode.run(data, decoder) {
    Ok(value) -> then(value)
    Error(reason) -> wisp.bad_request(string.inspect(reason))
  }
}

fn server(_context) {
  aide.Server(
    implementation: definitions.Implementation(
      name: "math_server",
      version: "0.1.0",
      title: Some("Math Server"),
    ),
    tools: mcp.tools(),
    resources: [],
    resource_templates: [],
    prompts: [],
  )
}

This server has the same tools for all users.

3. Implement server functionality.

Now we get to the business end of our MCP server.

Aide chooses an continuation passing style to implement the server functionality. This makes supporting JavaScript and BEAM runtimes easier. The details don’t matter here as our example doesn’t require promise or result types.

In the this snippet call_tool, read_resource, get_prompt and complete are all functions we need to implement. As this server only implements tools we can create noop versions of all functions but call_tool.

// math_server/mcp
import aide
import aide/effect
import aide/tool
import gleam/dict
import gleam/dynamic/decode
import gleam/int
import gleam/result
import oas/generator/utils
import oas/json_schema

pub fn handle(mcp_request, server) {
  case aide.handle_rpc(mcp_request, server) {
    effect.Done(result) -> result
    effect.CallTool(tool:, resume:) -> resume(call_tool(tool))
    effect.ReadResource(resource:, resume:) -> resume(read_resource(resource))
    effect.GetPrompt(prompt:, resume:) -> resume(get_prompt(prompt))
    effect.Complete(ref:, argument:, context:, resume:) ->
      resume(complete(ref, argument, context))
  }
}

// continued in Implement tools

4. Implement tools

We need to implement the tools we want to expose to the client. A tool in MCP has a specification and a decoder to decode the input from the client. The specification is used to generate the OpenAPI schema for the tool, that the LLM will understand.

Check out aide_generate to create encoders/decoders from the tool input/output schemas.

MCP requires that input and output are objects. There for the input/output schemas expect a list of json_schema fields, so you can’t accidentally specify an invalid schema.

// ...
// math_server/mcp

pub type Tool {
  Random
  Add(Int, Int)
}

pub fn tools() {
  [
    tool.Tool(
      spec: tool.Spec(
        name: "random",
        title: "Generate Random",
        description: "Generate a random number between two numbers",
        input: [],
        output: [json_schema.field("number", json_schema.integer())],
      ),
      decoder: decode.success(Random),
    ),
    tool.Tool(
      spec: tool.Spec(
        name: "add",
        title: "Add",
        description: "Add two numbers",
        input: [
          json_schema.field("x", json_schema.integer()),
          json_schema.field("y", json_schema.integer()),
        ],
        output: [json_schema.field("sum", json_schema.integer())],
      ),
      decoder: {
        use x <- decode.field("x", decode.int)
        use y <- decode.field("y", decode.int)
        decode.success(Add(x, y))
      },
    ),
  ]
}

fn call_tool(tool) {
  case tool {
    Random -> {
      use number <- result.map(math.random())
      dict.from_list([#("number", utils.Integer(number))])
    }
    Add(x, y) -> {
      use number <- result.map(math.add(x, y))
      dict.from_list([#("number", utils.Integer(number))])
    }
  }
}

Development

gleam test 

MCP definitions

The module aide/definitions is generated from a JSON Schema specification, maintained by the MCP project. To run the generation run.

gleam run dev

OAS doesn’t support list of types.

The Request type is not referenced anywhere in the definitions, but is used to specify a JSON RPC request Most other XRequest definitions are a contant and parameters

MCP

Model context protocol should allow for easy access from chats to tools and resources. This doesn’t seem to be the case so far.

Claude

Seems to ignore structured_content field in response and only use conent

Mistral

Connectors are limited to Gmail and Google Calendar for Free and Pro. There is no mention of custom connectors, although no details are shared about the enterprise level.

OpenAI

Needs a Pro or Team Plan

https://aaronparecki.com/articles

Credit

Created for EYG, a new integration focused programming language.

Search Document