Skip to content

Repository files navigation

main logo
Cortex

Build MCP Servers Declaratively in Golang

Go ReferenceGo Report CardGo WorkflowLicense: Apache 2.0Contributors

Table of Contents

Overview

The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. Cortex implements the full MCP specification, making it easy to:

  • Build MCP servers that expose resources and tools
  • Use standard transports like stdio and Server-Sent Events (SSE)
  • Handle all MCP protocol messages and lifecycle events
  • Follow Go best practices and clean architecture principles
  • Embed Cortex into existing servers and applications

Note: Cortex is always updated to align with the latest MCP specification from spec.modelcontextprotocol.io/latest

Installation

go get github.com/FreePeak/cortex

Quickstart

Let's create a simple MCP server that exposes an echo tool:

package main
import (
"context""fmt""log""os""github.com/FreePeak/cortex/pkg/server""github.com/FreePeak/cortex/pkg/tools"
)
funcmain() {
// Create a logger that writes to stderr instead of stdout// This is critical for STDIO servers as stdout must only contain JSON-RPC messageslogger:=log.New(os.Stderr, "[cortex] ", log.LstdFlags)
// Create the servermcpServer:=server.NewMCPServer("Echo Server Example", "1.0.0", logger)
// Create an echo toolechoTool:=tools.NewTool("echo",
tools.WithDescription("Echoes back the input message"),
tools.WithString("message",
tools.Description("The message to echo back"),
tools.Required(),
),
)
// Example of a tool with array parameterarrayExampleTool:=tools.NewTool("array_example",
tools.WithDescription("Example tool with array parameter"),
tools.WithArray("values",
tools.Description("Array of string values"),
tools.Required(),
tools.Items(map[string]interface{}{
"type": "string",
}),
),
)
// Add the tools to the server with handlersctx:=context.Background()
err:=mcpServer.AddTool(ctx, echoTool, handleEcho)
iferr!=nil {
logger.Fatalf("Error adding tool: %v", err)
}
err=mcpServer.AddTool(ctx, arrayExampleTool, handleArrayExample)
iferr!=nil {
logger.Fatalf("Error adding array example tool: %v", err)
}
// Write server status to stderr instead of stdout to maintain clean JSON protocolfmt.Fprintf(os.Stderr, "Starting Echo Server...\n")
fmt.Fprintf(os.Stderr, "Send JSON-RPC messages via stdin to interact with the server.\n")
fmt.Fprintf(os.Stderr, `Try: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo","parameters":{"message":"Hello, World!"}}}\n`)
// Serve over stdioiferr:=mcpServer.ServeStdio(); err!=nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
// Echo tool handlerfunchandleEcho(ctx context.Context, request server.ToolCallRequest) (interface{}, error) {
// Extract the message parametermessage, ok:=request.Parameters["message"].(string)
if!ok {
returnnil, fmt.Errorf("missing or invalid 'message' parameter")
}
// Return the echo response in the format expected by the MCP protocolreturnmap[string]interface{}{
"content": []map[string]interface{}{
{
"type": "text",
"text": message,
},
},
}, nil
}
// Array example tool handlerfunchandleArrayExample(ctx context.Context, request server.ToolCallRequest) (interface{}, error) {
// Extract the values parametervalues, ok:=request.Parameters["values"].([]interface{})
if!ok {
returnnil, fmt.Errorf("missing or invalid 'values' parameter")
}
// Convert values to string arraystringValues:=make([]string, len(values))
fori, v:=rangevalues {
stringValues[i] =v.(string)
}
// Return the array response in the format expected by the MCP protocolreturnmap[string]interface{}{
"content": stringValues,
}, nil
}

What is MCP?

The Model Context Protocol (MCP) is a standardized protocol that allows applications to provide context for LLMs in a secure and efficient manner. It separates the concerns of providing context and tools from the actual LLM interaction. MCP servers can:

  • Expose data through Resources (read-only data endpoints)
  • Provide functionality through Tools (executable functions)
  • Define interaction patterns through Prompts (reusable templates)
  • Support various transport methods (stdio, HTTP/SSE)

Core Concepts

Server

The MCP Server is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing:

// Create a new MCP server with loggermcpServer:=server.NewMCPServer("My App", "1.0.0", logger)

Tools

Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects:

// Define a calculator toolcalculatorTool:=tools.NewTool("calculator",
tools.WithDescription("Performs basic math operations"),
tools.WithString("operation",
tools.Description("The operation to perform (add, subtract, multiply, divide)"),
tools.Required(),
),
tools.WithNumber("a", tools.Description("First operand"),
tools.Required(),
),
tools.WithNumber("b", tools.Description("Second operand"),
tools.Required(),
),
)
// Add the tool to the server with a handlermcpServer.AddTool(ctx, calculatorTool, handleCalculator)

Providers

Providers allow you to group related tools and resources into a single package that can be easily registered with a server:

// Create a weather providerweatherProvider, err:=weather.NewWeatherProvider(logger)
iferr!=nil {
logger.Fatalf("Failed to create weather provider: %v", err)
}
// Register the provider with the servererr=mcpServer.RegisterProvider(ctx, weatherProvider)
iferr!=nil {
logger.Fatalf("Failed to register weather provider: %v", err)
}

Resources

Resources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects:

// Create a resource (Currently using the internal API)resource:=&domain.Resource{
URI: "sample://hello-world",
Name: "Hello World Resource",
Description: "A sample resource for demonstration purposes",
MIMEType: "text/plain",
}

Prompts

Prompts are reusable templates that help LLMs interact with your server effectively:

// Create a prompt (Currently using the internal API)codeReviewPrompt:=&domain.Prompt{
Name: "review-code",
Description: "A prompt for code review",
Template: "Please review this code:\n\n{{.code}}",
Parameters: []domain.PromptParameter{
{
Name: "code",
Description: "The code to review",
Type: "string",
Required: true,
},
},
}
// Note: Prompt support is being updated in the public API

Running Your Server

MCP servers in Go can be connected to different transports depending on your use case:

STDIO

For command-line tools and direct integrations:

// Start a stdio serveriferr:=mcpServer.ServeStdio(); err!=nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

IMPORTANT: When using STDIO, all logs must be directed to stderr to maintain the clean JSON-RPC protocol on stdout:

// Create a logger that writes to stderrlogger:=log.New(os.Stderr, "[cortex] ", log.LstdFlags)
// All debug/status messages should use stderrfmt.Fprintf(os.Stderr, "Server starting...\n")

HTTP with SSE

For web applications, you can use Server-Sent Events (SSE) for real-time communication:

// Configure the HTTP addressmcpServer.SetAddress(":8080")
// Start an HTTP server with SSE supportiferr:=mcpServer.ServeHTTP(); err!=nil {
log.Fatalf("HTTP server error: %v", err)
}
// For graceful shutdownctx, cancel:=context.WithTimeout(context.Background(), 10*time.Second)
defercancel()
iferr:=mcpServer.Shutdown(ctx); err!=nil {
log.Fatalf("Server shutdown error: %v", err)
}

Multi-Protocol

You can also run multiple protocol servers simultaneously by using goroutines:

// Start an HTTP servergofunc() {
iferr:=mcpServer.ServeHTTP(); err!=nil {
log.Fatalf("HTTP server error: %v", err)
}
}()
// Start a STDIO servergofunc() {
iferr:=mcpServer.ServeStdio(); err!=nil {
log.Fatalf("STDIO server error: %v", err)
}
}()
// Wait for shutdown signalstop:=make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop

Testing and Debugging

For more detailed information on testing and debugging Cortex servers, see the Testing Guide.

Embedding Cortex

Cortex can be embedded into existing applications to add MCP capabilities without running a separate server. This is useful for integrating with existing web frameworks or applications like PocketBase.

HTTP Server Integration

You can easily integrate Cortex with any Go HTTP server:

package main
import (
"log""net/http""os""github.com/FreePeak/cortex/pkg/server""github.com/FreePeak/cortex/pkg/tools"
)
funcmain() {
// Create a loggerlogger:=log.New(os.Stderr, "[cortex] ", log.LstdFlags)
// Create an MCP servermcpServer:=server.NewMCPServer("Embedded MCP Server", "1.0.0", logger)
// Add some toolsechoTool:=tools.NewTool("echo",
tools.WithDescription("Echoes back the input message"),
tools.WithString("message",
tools.Description("The message to echo back"),
tools.Required(),
),
)
// Add the tool to the servermcpServer.AddTool(context.Background(), echoTool, func(ctx context.Context, request server.ToolCallRequest) (interface{}, error) {
message:=request.Parameters["message"].(string)
returnmap[string]interface{}{
"content": []map[string]interface{}{
{
"type": "text",
"text": message,
},
},
}, nil
})
// Create an HTTP adapter for the MCP serveradapter:=server.NewHTTPAdapter(mcpServer, server.WithPath("/api/mcp"))
// Use the adapter in your HTTP serverhttp.Handle("/api/mcp/", adapter.Handler())
// Add your other routeshttp.HandleFunc("/", func(w http.ResponseWriter, r*http.Request) {
w.Write([]byte("Hello from the main server!"))
})
// Start the serverlogger.Println("Starting server on :8080")
http.ListenAndServe(":8080", nil)
}

PocketBase Integration

Cortex can be integrated with PocketBase, an open-source backend with database, auth, and admin UI:

package main
import (
"log""github.com/pocketbase/pocketbase""github.com/pocketbase/pocketbase/core""github.com/FreePeak/cortex/pkg/integration/pocketbase""github.com/FreePeak/cortex/pkg/tools"
)
funcmain() {
// Create a new PocketBase appapp:=pocketbase.New()
// Initialize Cortex pluginplugin:=pocketbase.NewCortexPlugin(
pocketbase.WithName("PocketBase MCP Server"),
pocketbase.WithVersion("1.0.0"),
pocketbase.WithBasePath("/api/mcp"),
)
// Add tools to the pluginechoTool:=tools.NewTool("echo",
tools.WithDescription("Echoes back the input message"),
tools.WithString("message",
tools.Description("The message to echo back"),
tools.Required(),
),
)
// Add the tool with a handlerplugin.AddTool(echoTool, func(ctx context.Context, request pocketbase.ToolCallRequest) (interface{}, error) {
message:=request.Parameters["message"].(string)
returnmap[string]interface{}{
"content": []map[string]interface{}{
{
"type": "text",
"text": message,
},
},
}, nil
})
// Register the plugin with PocketBaseapp.OnBeforeServe().Add(func(e*core.ServeEvent) error {
// Register the pluginreturnplugin.RegisterWithPocketBase(app)
})
// Start the PocketBase appiferr:=app.Start(); err!=nil {
log.Fatal(err)
}
}

For more detailed documentation on embedding Cortex, see the Embedding Guide.

Examples

Basic Examples

The repository includes several basic examples in the examples directory:

  • STDIO Server: A simple MCP server that communicates via STDIO (examples/stdio-server)
  • SSE Server: A server that uses HTTP with Server-Sent Events for communication (examples/sse-server)
  • Multi-Protocol: A server that can run on multiple protocols simultaneously (examples/multi-protocol)

Advanced Examples

The examples directory also includes more advanced use cases:

  • Providers: Examples of how to create and use providers to organize related tools (examples/providers)
    • Weather Provider: Demonstrates how to create a provider for weather-related tools
    • Database Provider: Shows how to create a provider for database operations

Plugin System

Cortex includes a plugin system for extending server capabilities:

// Create a new provider based on the BaseProvidertypeMyProviderstruct {
*plugin.BaseProvider
}
// Create a new provider instancefuncNewMyProvider(logger*log.Logger) (*MyProvider, error) {
info:= plugin.ProviderInfo{
ID: "my-provider",
Name: "My Provider",
Version: "1.0.0",
Description: "A custom provider for my tools",
Author: "Your Name",
URL: "https://github.com/yourusername/myrepo",
}
baseProvider:=plugin.NewBaseProvider(info, logger)
provider:=&MyProvider{
BaseProvider: baseProvider,
}
// Register tools with the provider// ...returnprovider, nil
}

Package Structure

The Cortex codebase is organized into several packages:

  • pkg/server: Core server implementation
  • pkg/tools: Tool creation and management
  • pkg/plugin: Plugin system for extending server capabilities
  • pkg/types: Common types and interfaces
  • pkg/builder: Builders for creating complex objects

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

Support & Contact

Buy Me A Coffee

About

A declarative platform for building Model Context Protocol (MCP) servers in Golang—exposing tools, resources & prompts in a clean, structured way

Topics

Resources

Stars

25 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages