Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tutorial: Build an MCP server

This tutorial builds a small MCP server in Harn. The same program can expose tools, static resources, resource templates, and prompts over stdio.

Use the companion example as a baseline:

cargo run --bin harn -- mcp-serve examples/mcp_server.harn

1. Register tools

Start by creating a tool registry and attaching a few tools with explicit schemas:

pipeline main(task) {
  var tools = tool_registry()

  tools = tool_define(tools, "greet", "Greet someone by name", {
    params: { name: "string" },
    handler: { args -> "Hello, " + args.name + "!" },
    annotations: {
      title: "Greeting Tool",
      readOnlyHint: true,
      destructiveHint: false,
    }
  })

  tools = tool_define(tools, "add", "Add two numbers", {
    params: { a: "number", b: "number" },
    handler: { args -> to_string(args.a + args.b) }
  })

  mcp_tools(tools)
}

Keep tool names short and descriptive. The description should be written for a model, not for a human reading source code.

2. Add resources and templates

Resources are good for static content, while resource templates are better for parameterized data.

pipeline main(task) {
  mcp_resource({
    uri: "docs://readme",
    name: "README",
    mime_type: "text/markdown",
    text: "# Harn MCP Demo\n\nThis server is implemented in Harn."
  })

  mcp_resource_template({
    uri_template: "config://{key}",
    name: "Configuration values",
    mime_type: "text/plain",
    handler: { args ->
      if args.key == "version" {
        "0.6.0"
      } else if args.key == "name" {
        "harn-demo"
      } else {
        "unknown key: " + args.key
      }
    }
  })
}

That pattern is useful for docs, policy data, generated summaries, and other state you want to expose without writing a dedicated tool for each lookup.

3. Add prompts

Prompts let the client ask the server for structured guidance:

pipeline main(task) {
  mcp_prompt({
    name: "code_review",
    description: "Review code for correctness and maintainability",
    arguments: [
      { name: "code", description: "The code to review", required: true },
      { name: "language", description: "Programming language" }
    ],
    handler: { args ->
      let lang = args.language ?? "unknown"
      "Please review this " + lang + " code for correctness, bugs, and tests:\n\n" + args.code
    }
  })
}

Prompts are a good way to standardize a client workflow while still letting the client supply the final payload.

4. Run it over stdio

Once the pipeline calls mcp_tools(), mcp_resource(), or mcp_prompt(), launch the server with:

harn mcp-serve examples/mcp_server.harn

All user-visible output goes to stderr; the MCP transport stays on stdout. That keeps the server compatible with Claude Desktop, Cursor, and other MCP clients.

5. Keep the surface small

A good MCP server has a narrow surface area:

  • expose only the operations the client truly needs
  • keep tool names and schemas stable
  • prefer explicit resources over ad hoc text blobs
  • use resource templates when one static resource is not enough

If you want the server to be consumable from a desktop client, add a short launch snippet in the client config and test the tool list before expanding the surface.