Skip to content

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

License

Notifications You must be signed in to change notification settings

FreePeak/cortex

Repository files navigation

main logo
Cortex

Build MCP Servers Declaratively in Golang

Go Reference Go Report Card Go Workflow License: Apache 2.0 Contributors

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"
)

func main() {
	// Create a logger that writes to stderr instead of stdout
	// This is critical for STDIO servers as stdout must only contain JSON-RPC messages
	logger := log.New(os.Stderr, "[cortex] ", log.LstdFlags)

	// Create the server
	mcpServer := server.NewMCPServer("Echo Server Example", "1.0.0", logger)

	// Create an echo tool
	echoTool := 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 parameter
	arrayExampleTool := 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 handlers
	ctx := context.Background()
	err := mcpServer.AddTool(ctx, echoTool, handleEcho)
	if err != nil {
		logger.Fatalf("Error adding tool: %v", err)
	}

	err = mcpServer.AddTool(ctx, arrayExampleTool, handleArrayExample)
	if err != nil {
		logger.Fatalf("Error adding array example tool: %v", err)
	}

	// Write server status to stderr instead of stdout to maintain clean JSON protocol
	fmt.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 stdio
	if err := mcpServer.ServeStdio(); err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		os.Exit(1)
	}
}

// Echo tool handler
func handleEcho(ctx context.Context, request server.ToolCallRequest) (interface{}, error) {
	// Extract the message parameter
	message, ok := request.Parameters["message"].(string)
	if !ok {
		return nil, fmt.Errorf("missing or invalid 'message' parameter")
	}

	// Return the echo response in the format expected by the MCP protocol
	return map[string]interface{}{
		"content": []map[string]interface{}{
			{
				"type": "text",
				"text": message,
			},
		},
	}, nil
}

// Array example tool handler
func handleArrayExample(ctx context.Context, request server.ToolCallRequest) (interface{}, error) {
	// Extract the values parameter
	values, ok := request.Parameters["values"].([]interface{})
	if !ok {
		return nil, fmt.Errorf("missing or invalid 'values' parameter")
	}

	// Convert values to string array
	stringValues := make([]string, len(values))
	for i, v := range values {
		stringValues[i] = v.(string)
	}

	// Return the array response in the format expected by the MCP protocol
	return map[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 logger
mcpServer := 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 tool
calculatorTool := 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 handler
mcpServer.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 provider
weatherProvider, err := weather.NewWeatherProvider(logger)
if err != nil {
    logger.Fatalf("Failed to create weather provider: %v", err)
}

// Register the provider with the server
err = mcpServer.RegisterProvider(ctx, weatherProvider)
if err != 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 server
if err := 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 stderr
logger := log.New(os.Stderr, "[cortex] ", log.LstdFlags)

// All debug/status messages should use stderr
fmt.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 address
mcpServer.SetAddress(":8080")

// Start an HTTP server with SSE support
if err := mcpServer.ServeHTTP(); err != nil {
    log.Fatalf("HTTP server error: %v", err)
}

// For graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := 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 server
go func() {
    if err := mcpServer.ServeHTTP(); err != nil {
        log.Fatalf("HTTP server error: %v", err)
    }
}()

// Start a STDIO server
go func() {
    if err := mcpServer.ServeStdio(); err != nil {
        log.Fatalf("STDIO server error: %v", err)
    }
}()

// Wait for shutdown signal
stop := 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"
)

func main() {
	// Create a logger
	logger := log.New(os.Stderr, "[cortex] ", log.LstdFlags)

	// Create an MCP server
	mcpServer := server.NewMCPServer("Embedded MCP Server", "1.0.0", logger)

	// Add some tools
	echoTool := 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 server
	mcpServer.AddTool(context.Background(), echoTool, func(ctx context.Context, request server.ToolCallRequest) (interface{}, error) {
		message := request.Parameters["message"].(string)
		return map[string]interface{}{
			"content": []map[string]interface{}{
				{
					"type": "text",
					"text": message,
				},
			},
		}, nil
	})

	// Create an HTTP adapter for the MCP server
	adapter := server.NewHTTPAdapter(mcpServer, server.WithPath("/api/mcp"))

	// Use the adapter in your HTTP server
	http.Handle("/api/mcp/", adapter.Handler())
	
	// Add your other routes
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello from the main server!"))
	})

	// Start the server
	logger.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"
)

func main() {
	// Create a new PocketBase app
	app := pocketbase.New()

	// Initialize Cortex plugin
	plugin := pocketbase.NewCortexPlugin(
		pocketbase.WithName("PocketBase MCP Server"),
		pocketbase.WithVersion("1.0.0"),
		pocketbase.WithBasePath("/api/mcp"),
	)

	// Add tools to the plugin
	echoTool := 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 handler
	plugin.AddTool(echoTool, func(ctx context.Context, request pocketbase.ToolCallRequest) (interface{}, error) {
		message := request.Parameters["message"].(string)
		return map[string]interface{}{
			"content": []map[string]interface{}{
				{
					"type": "text",
					"text": message,
				},
			},
		}, nil
	})

	// Register the plugin with PocketBase
	app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
		// Register the plugin
		return plugin.RegisterWithPocketBase(app)
	})

	// Start the PocketBase app
	if err := 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 BaseProvider
type MyProvider struct {
    *plugin.BaseProvider
}

// Create a new provider instance
func NewMyProvider(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
    // ...
    
    return provider, 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

License

Stars

Watchers

Forks

Sponsor this project

Packages

No packages published

Languages