- Overview
- Installation
- Quickstart
- What is MCP?
- Core Concepts
- Running Your Server
- Embedding Cortex
- Examples
- Package Structure
- Contributing
- License
- Support & Contact
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
go get github.com/FreePeak/cortexLet'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
}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)
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 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 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 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 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 APIMCP servers in Go can be connected to different transports depending on your use case:
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")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)
}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)
<-stopFor more detailed information on testing and debugging Cortex servers, see the Testing Guide.
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.
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)
}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.
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)
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
 
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
}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
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (git checkout -b feature/amazing-feature)
- Commit your changes (git commit -m 'Add some amazing feature')
- Push to the branch (git push origin feature/amazing-feature)
- Open a Pull Request
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
- For questions or issues, email mnhatlinh.doan@gmail.com
- Open an issue directly: Issue Tracker
- If Cortex helps your work, please consider supporting: