├── .DS_Store ├── .cursor ├── mcp.example.json ├── mcp.json ├── rules │ ├── go-architecture.mdc │ └── go-convention.mdc └── settings.json ├── .github ├── FUNDING.yml └── workflows │ ├── go.yml │ └── golangci.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── SUMMARY.md ├── agent-sdk-test ├── array-parameter-test ├── directory-structure.md ├── docs ├── cortex-client-interaction-flow.md ├── embedding.md ├── implementation-plan.md ├── implementation-status.md └── pocketbase-integration.md ├── examples ├── README.md ├── agent-sdk-test │ └── main.go ├── array-parameter-test │ └── main.go ├── integration │ └── pocketbase │ │ ├── README.md │ │ ├── kill-port.sh │ │ ├── main.go │ │ ├── pocketbase │ │ ├── run.sh │ │ ├── test-client.html │ │ └── test-client.js ├── multi-protocol │ ├── README.md │ └── main.go ├── providers │ ├── README.md │ ├── database │ │ └── provider.go │ └── weather │ │ └── provider.go ├── sse-server │ ├── README.md │ └── main.go └── stdio-server │ ├── README.md │ ├── logs │ ├── cortex-20250401-003240.log │ └── cortex-20250401-003528.log │ ├── main.go │ └── test-stdio-server ├── fix-imports.sh ├── go.mod ├── go.sum ├── internal ├── builder │ ├── serverbuilder.go │ └── serverbuilder_test.go ├── domain │ ├── errors.go │ ├── errors_test.go │ ├── jsonrpc_models.go │ ├── jsonrpc_models_test.go │ ├── mocks_test.go │ ├── notification_models.go │ ├── notification_models_test.go │ ├── repositories.go │ ├── sse_interfaces.go │ ├── sse_interfaces_test.go │ ├── types.go │ └── types_test.go ├── infrastructure │ ├── logging │ │ ├── README.md │ │ ├── example_test.go │ │ ├── integration_example.go │ │ ├── logger.go │ │ └── logger_test.go │ └── server │ │ ├── errors.go │ │ ├── inmemory.go │ │ ├── inmemory_test.go │ │ ├── notification.go │ │ ├── notification_test.go │ │ ├── sse_connection_manager.go │ │ ├── sse_connection_manager_test.go │ │ ├── sse_handler.go │ │ ├── sse_server.go │ │ ├── sse_server_test.go │ │ ├── sse_session.go │ │ └── sse_session_test.go ├── interfaces │ ├── rest │ │ └── server.go │ └── stdio │ │ ├── options.go │ │ └── server.go ├── tools │ └── tools.go └── usecases │ ├── server.go │ └── server_test.go ├── logo.svg ├── logs └── cortex-20250401-003412.log ├── pkg ├── README.md ├── builder │ ├── server_builder.go │ └── tool_handler.go ├── integration │ └── pocketbase │ │ ├── plugin.go │ │ └── plugin_test.go ├── plugin │ ├── README.md │ ├── base_provider.go │ ├── interface.go │ └── registry.go ├── server │ ├── embeddable.go │ ├── embeddable_test.go │ ├── http_adapter.go │ └── server.go ├── tools │ └── helper.go └── types │ └── types.go ├── repository_diagram.md ├── res.json ├── test-call.sh ├── todo.md └── update-imports.sh /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreePeak/cortex/64e47c1d4a73c1fe9fc2491c4ad49b9cd8c0aeb2/.DS_Store -------------------------------------------------------------------------------- /.cursor/mcp.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "golang-mcp-server-sse": { 4 | "url": "http://localhost:8080/sse" 5 | }, 6 | "golang-mcp-server-stdio": { 7 | "command": "./bin/echo-stdio-server", 8 | "args": [">", "./run.log"] 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /.cursor/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "pocketbase": { 4 | "url": "http://localhost:8090/api/mcp/sse" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /.cursor/rules/go-convention.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # Golang Guidelines 7 | - Follow the Go style guide (https://go.dev/doc/effective_go). 8 | - Use lowercase package names (e.g., `http`, `utils`). 9 | - Prefer explicit error handling with `if err != nil` over panics. 10 | - Use structs with exported fields (capitalized) for public APIs; lowercase for internal use. 11 | - Avoid unnecessary interfaces; only define them when multiple implementations are needed. 12 | - Use `go fmt` style: no unnecessary whitespace, consistent indentation. 13 | - Structure files: one main type/struct per file with related methods, helpers in separate utils files. 14 | - Naming: Short, concise, and contextual (e.g., `db` for database, `srv` for server). 15 | - Applying the Design Pattern when see the use case is fit with any design pattern. 16 | 17 | 18 | # Golang IDE Cursor Rules 19 | 20 | These rules govern how the cursor behaves in your IDE to ensure that all generated Golang code passes linters (`golint`), static analysis (`go vet`), and can run without issues. 21 | 22 | ## Automatic Imports 23 | 24 | 1. **Auto-import**: When a new package is referenced, the cursor automatically adds the import to the top of the file 25 | 2. **Clean imports**: When removing code that uses an import, the cursor automatically removes unused imports 26 | 3. **Group imports**: Standard library imports are grouped first, followed by third-party libraries, then local packages 27 | 4. **Alphabetical order**: Imports within each group are sorted alphabetically 28 | 29 | ## Indentation and Formatting 30 | 31 | 1. **Tab-based indentation**: Always use tabs (not spaces) for indentation 32 | 2. **Auto-format on save**: Always run `gofmt` when saving a file 33 | 3. **Standard bracing**: Opening braces always on the same line as the statement: 34 | ```go 35 | if condition { 36 | // Code 37 | } 38 | ``` 39 | 4. **Line length**: Hard wrap at 100 characters 40 | 41 | ## Variable and Function Naming 42 | 43 | 1. **camelCase for private**: Private variables and functions use camelCase 44 | 2. **PascalCase for exported**: Exported variables and functions use PascalCase 45 | 3. **Acronyms**: Acronyms in names are all uppercase (e.g., `HTTPServer` not `HttpServer`) 46 | 4. **Short-lived variables**: Use short names for variables with small scopes 47 | 5. **Descriptive names**: Use descriptive names for exported functions and variables 48 | 6. **Context parameter**: Name the context parameter `ctx` when present 49 | 50 | ## Error Handling 51 | 52 | 1. **Error checking**: Every error must be checked - cursor enforces checking after functions that return errors 53 | 2. **Error documentation**: Mandatory error documentation for exported functions 54 | 3. **Error wrapping**: Use `fmt.Errorf("context: %w", err)` for wrapping errors 55 | 4. **Early returns**: The cursor favors early returns for error conditions, reducing nesting 56 | 57 | ## Comments and Documentation 58 | 59 | 1. **Package documentation**: Every package has documentation in a separate `doc.go` file 60 | 2. **Function documentation**: All exported functions have godoc-compatible documentation 61 | 3. **Linting comments**: When cursor detects a comment that doesn't start with the name of the thing being commented, it's automatically fixed 62 | 4. **TODO format**: Standard format for TODOs: `// TODO(username): explanation` 63 | 64 | ## Code Structure 65 | 66 | 1. **File organization**: Types come first, followed by constants, variables, then functions 67 | 2. **Interface consistency**: Method declarations in interfaces match the same order as their implementations 68 | 3. **Balanced grouping**: Related constants and variables are grouped together 69 | 4. **Method ordering**: Methods for the same type are grouped together 70 | 71 | ## Testing 72 | 73 | 1. **Auto-create test**: When creating a new function, the cursor offers to create a corresponding test file 74 | 2. **Table-driven tests**: The cursor prefers table-driven test templates 75 | 3. **Descriptive test names**: Test functions follow the format `TestSubject_Action_Condition` 76 | 4. **Test helper functions**: Helper functions are marked with `t.Helper()` 77 | 78 | ## Concurrency Patterns 79 | 80 | 1. **Mutex naming**: Name mutex variables with a `mu` prefix 81 | 2. **Context first**: In function parameters, context is always the first parameter 82 | 3. **Go routine boundary**: When spawning a goroutine, cursor automatically adds comments about responsibility for closure variables 83 | 4. **Channel direction**: Channels always specify direction when used as parameters: 84 | ```go 85 | func consume(ch <-chan int) {} // Receive-only 86 | func produce(ch chan<- int) {} // Send-only 87 | ``` 88 | 89 | ## Safe Code Patterns 90 | 91 | 1. **Nil checks**: Cursor enforces nil checks before dereferencing pointers 92 | 2. **Range copy check**: Warns when using a range variable in a goroutine to prevent closure issues 93 | 3. **Struct initialization**: Enforces field names in struct initialization for clarity 94 | 4. **Blank identifier**: Prompts for a comment when using blank identifier (`_`) to explain why 95 | 96 | ## IDE Integration Features 97 | 98 | 1. **Auto-completion**: Intelligent suggestions for functions and variables 99 | 2. **Hover information**: Show documentation, type information, and potential issues on hover 100 | 3. **Jump to definition**: Quick navigation to declarations and implementations 101 | 4. **Find usages**: Locate all references to a symbol across the codebase 102 | 5. **Refactoring tools**: Rename, extract function, and other refactorings with preview 103 | 6. **Error diagnostics**: Real-time error highlighting with suggestions for fixes 104 | 7. **Auto-implement interfaces**: Generate method stubs for implementing interfaces 105 | 106 | ## Pre-Commit Checks 107 | 108 | 1. **Automated pre-commit**: Run `golint`, `go vet`, and `go test` before allowing commits 109 | 2. **Dependency check**: Verify dependencies are properly managed and vendored 110 | 3. **License header**: Ensure all new files have the appropriate license header 111 | 4. **Coverage check**: Verify that new code has sufficient test coverage 112 | 113 | 114 | -------------------------------------------------------------------------------- /.cursor/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcp.executablePath": "/Users/harvey/Work/dev/FreePeak/Opensource/cortex/bin/stdio-server", 3 | "mcp.transport": "stdio", 4 | "mcp.logging": true, 5 | "mcp.logLevel": "debug" 6 | } 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: linhdmn 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | lint: 11 | name: Lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v3 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v4 19 | with: 20 | go-version: '1.21' 21 | cache: true 22 | 23 | - name: golangci-lint 24 | uses: golangci/golangci-lint-action@v3 25 | with: 26 | version: latest 27 | args: --timeout=5m 28 | # Use custom configuration 29 | only-new-issues: true 30 | # Copy the config from our local file 31 | skip-cache: true 32 | 33 | test: 34 | name: Test 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Checkout code 38 | uses: actions/checkout@v3 39 | 40 | - name: Set up Go 41 | uses: actions/setup-go@v4 42 | with: 43 | go-version: '1.21' 44 | cache: true 45 | 46 | - name: Install dependencies 47 | run: go mod download 48 | 49 | - name: Run tests 50 | run: go test -v -race -cover ./... 51 | 52 | build: 53 | name: Build 54 | runs-on: ubuntu-latest 55 | needs: [lint, test] 56 | steps: 57 | - name: Checkout code 58 | uses: actions/checkout@v3 59 | 60 | - name: Set up Go 61 | uses: actions/setup-go@v4 62 | with: 63 | go-version: '1.21' 64 | cache: true 65 | 66 | - name: Install dependencies 67 | run: go mod download 68 | 69 | - name: Build 70 | run: go build -v ./... -------------------------------------------------------------------------------- /.github/workflows/golangci.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | jobs: 8 | golangci: 9 | name: lint 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-go@v5 14 | with: 15 | go-version: '1.24' 16 | cache: false 17 | - name: golangci-lint 18 | uses: golangci/golangci-lint-action@v6 19 | with: 20 | args: --timeout=5m 21 | version: latest 22 | # Optional: show only new issues if it's a pull request. The default value is `false`. 23 | only-new-issues: true 24 | # Optional: if set to true then the all caching functionality will be complete disabled, 25 | # takes precedence over all other caching options. 26 | skip-cache: true 27 | # Optional: the working directory, useful for monorepos. 28 | # working-directory: somedir 29 | # Optional: configuration file for golangci-lint 30 | config: .golangci.yml 31 | # options from https://github.com/golangci/golangci-lint-action 32 | golangci-lint-flags: "--config=.golangci.yml --issues-exit-code=0 --timeout=8m --out-format=line-number" 33 | # Additional envs 34 | env: 35 | GO111MODULE: on 36 | GOPROXY: https://proxy.golang.org 37 | GOSUMDB: sum.golang.org 38 | args: --timeout=10m --local-prefixes=github.com/FreePeak/cortex 39 | 40 | run: 41 | timeout: 5m 42 | modules-download-mode: readonly 43 | tests: true 44 | 45 | linters: 46 | enable: 47 | - errcheck 48 | - gofmt 49 | - gosimple 50 | - govet 51 | - ineffassign 52 | - misspell 53 | - staticcheck 54 | - typecheck 55 | - unused 56 | - whitespace 57 | 58 | linters-settings: 59 | goimports: 60 | local-prefixes: github.com/FreePeak/cortex 61 | 62 | issues: 63 | exclude-rules: 64 | - path: _test\.go 65 | linters: 66 | - errcheck 67 | - path: internal/interfaces/rest 68 | linters: 69 | - goimports 70 | 71 | max-issues-per-linter: 0 72 | max-same-issues: 0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/* 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | skip-files: 4 | - ".*\\.pb\\.go$" 5 | allow-parallel-runners: true 6 | go: '1.24' 7 | 8 | linters-settings: 9 | errcheck: 10 | check-type-assertions: true 11 | govet: 12 | check-shadowing: true 13 | gofmt: 14 | simplify: true 15 | gocyclo: 16 | min-complexity: 15 17 | misspell: 18 | locale: US 19 | lll: 20 | line-length: 120 21 | revive: 22 | severity: warning 23 | confidence: 0.8 24 | gocritic: 25 | enabled-tags: 26 | - performance 27 | goimports: 28 | local-prefixes: github.com/FreePeak/cortex 29 | 30 | linters: 31 | disable-all: true 32 | enable: 33 | - errcheck 34 | - gofmt 35 | - goimports 36 | - gosimple 37 | - govet 38 | - ineffassign 39 | - misspell 40 | - staticcheck 41 | - typecheck 42 | - unused 43 | 44 | issues: 45 | exclude-rules: 46 | - path: _test\.go 47 | linters: 48 | - errcheck 49 | - path: internal/interfaces/rest 50 | linters: 51 | - goimports 52 | - path: internal/interfaces/stdio/server\.go 53 | linters: 54 | - goimports 55 | - path: internal/infrastructure/server/sse_server_test\.go 56 | linters: 57 | - goimports 58 | - path: internal/infrastructure/server/sse_handler\.go 59 | linters: 60 | - goimports 61 | - path: internal/infrastructure/server/sse_server\.go 62 | linters: 63 | - goimports 64 | - path: internal/infrastructure/server/sse_session\.go 65 | linters: 66 | - goimports 67 | - path: internal/infrastructure/server/inmemory_test\.go 68 | linters: 69 | - goimports 70 | - path: internal/infrastructure/server/notification_test\.go 71 | linters: 72 | - goimports 73 | 74 | max-issues-per-linter: 0 75 | max-same-issues: 0 -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for cortex 2 | .PHONY: build test test-race lint clean run example-sse example-stdio example-multi coverage deps update-deps help 3 | 4 | # Go parameters 5 | GOCMD=go 6 | GOBUILD=$(GOCMD) build 7 | GOCLEAN=$(GOCMD) clean 8 | GOTEST=$(GOCMD) test 9 | GOGET=$(GOCMD) get 10 | GOMOD=$(GOCMD) mod 11 | 12 | # Example paths 13 | STDIO_SERVER=examples/stdio-server/main.go 14 | SSE_SERVER=examples/sse-server/main.go 15 | MULTI_PROTOCOL_SERVER=examples/multi-protocol/main.go 16 | POCKETBASE_SERVER=examples/pocketbase/main.go 17 | # Binary paths 18 | BIN_DIR=bin 19 | STDIO_BIN=$(BIN_DIR)/stdio-server 20 | SSE_BIN=$(BIN_DIR)/sse-server 21 | MULTI_BIN=$(BIN_DIR)/multi-protocolr-server 22 | 23 | help: 24 | @echo "Available commands:" 25 | @echo " make - Run tests and build binaries" 26 | @echo " make build - Build the server binaries" 27 | @echo " make test - Run tests with race detection and coverage" 28 | @echo " make test-race - Run tests with race detection and coverage" 29 | @echo " make coverage - Generate test coverage report" 30 | @echo " make lint - Run linter" 31 | @echo " make clean - Clean build artifacts" 32 | @echo " make deps - Tidy up dependencies" 33 | @echo " make update-deps - Update dependencies" 34 | @echo " make example-sse - Run example SSE server" 35 | @echo " make example-stdio - Run example stdio server" 36 | @echo " make example-multi - Run example multi-protocol server" 37 | 38 | all: test build lint 39 | 40 | build: $(BIN_DIR) 41 | $(GOBUILD) -o $(STDIO_BIN) $(STDIO_SERVER) 42 | $(GOBUILD) -o $(SSE_BIN) $(SSE_SERVER) 43 | $(GOBUILD) -o $(MULTI_BIN) $(MULTI_PROTOCOL_SERVER) 44 | 45 | $(BIN_DIR): 46 | mkdir -p $(BIN_DIR) 47 | 48 | example-sse: 49 | $(GOCMD) run $(SSE_SERVER) 50 | 51 | example-stdio: 52 | $(GOCMD) run $(STDIO_SERVER) 53 | 54 | example-multi: 55 | $(GOCMD) run $(MULTI_PROTOCOL_SERVER) -protocol stdio 56 | 57 | example-multi-http: 58 | $(GOCMD) run $(MULTI_PROTOCOL_SERVER) -protocol http -address localhost:8080 59 | 60 | example-pocketbase: 61 | $(GOCMD) run $(POCKETBASE_SERVER) -port 8090 62 | test: 63 | $(GOTEST) ./... -v -race -cover 64 | 65 | test-race: 66 | $(GOTEST) ./... -v -race -cover 67 | 68 | coverage: 69 | $(GOTEST) -cover -coverprofile=coverage.out ./... 70 | $(GOCMD) tool cover -html=coverage.out 71 | 72 | lint: 73 | golangci-lint run ./... 74 | 75 | clean: 76 | $(GOCLEAN) 77 | rm -rf $(BIN_DIR) 78 | rm -f coverage.out 79 | 80 | deps: 81 | $(GOMOD) tidy 82 | 83 | update-deps: 84 | $(GOMOD) tidy 85 | $(GOGET) -u ./... -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Cortex - Flexible MCP Server Platform 2 | 3 | ## Project Overview 4 | 5 | The Cortex project provides a flexible Model Context Protocol (MCP) server platform that supports dynamic tool registration and multiple communication protocols. The platform is designed with a clean separation between the core server infrastructure and the specific tool implementations. 6 | 7 | ## Key Features 8 | 9 | ### Plugin Architecture 10 | 11 | - **Provider Interface**: A standardized interface for tool providers 12 | - **Registry System**: Dynamic registration and discovery of tools 13 | - **Base Provider**: A foundation for building custom tool providers 14 | - **Flexible Server**: Support for both stdio and HTTP protocols 15 | 16 | ### Decoupled Design 17 | 18 | - **Clean Separation**: Core platform is separate from specific tool implementations 19 | - **External Tool Providers**: Tools can be implemented outside the core 20 | - **Dynamic Loading**: Tools can be registered at runtime 21 | - **Versioned Interfaces**: Support for evolving tool interfaces 22 | 23 | ### Communication Support 24 | 25 | - **Stdio Protocol**: Command-line interface via standard input/output 26 | - **HTTP Protocol**: Web-based interface via Server-Sent Events (SSE) 27 | - **Unified Message Handling**: Consistent API across protocols 28 | - **Graceful Shutdown**: Proper handling of termination signals 29 | 30 | ### Security Features 31 | 32 | - **Input Validation**: Validation of all tool parameters 33 | - **Error Handling**: Consistent error reporting and logging 34 | - **Provider Isolation**: Tools run in isolated provider contexts 35 | - **Session Management**: Client session tracking and management 36 | 37 | ## Implementation 38 | 39 | The implementation includes the following components: 40 | 41 | ### Core Platform (`pkg/plugin`, `pkg/server`) 42 | 43 | - **Plugin Interface**: Defines the contract for tool providers 44 | - **Plugin Registry**: Manages provider registration and discovery 45 | - **Base Provider**: Provides a foundation for building providers 46 | - **Flexible Server**: Coordinates tools and communication 47 | 48 | ### Example Providers (`examples/providers`) 49 | 50 | - **Weather Provider**: Demonstrates integration with a weather service 51 | - **Database Provider**: Shows implementation of database operations 52 | 53 | ### Example Server (`cmd/flexible-server`) 54 | 55 | - **Multi-Protocol Server**: Supports both stdio and HTTP 56 | - **Dynamic Tool Integration**: Demonstrates provider registration 57 | - **Command-Line Options**: Configuration for different operation modes 58 | - **Error Handling**: Robust error handling and logging 59 | 60 | ## Usage 61 | 62 | ### Running the Server 63 | 64 | ```bash 65 | # Run with stdio protocol 66 | ./run-flexible-stdio.sh 67 | 68 | # Run with HTTP protocol 69 | ./run-flexible-http.sh 70 | ``` 71 | 72 | ### Creating Custom Providers 73 | 74 | See the documentation in `pkg/plugin/README.md` and examples in `examples/providers/` for details on how to create custom tool providers. 75 | 76 | ## Documentation 77 | 78 | The project includes comprehensive documentation: 79 | 80 | - **READMEs**: Overview and usage instructions in each directory 81 | - **Code Comments**: Detailed comments for all public interfaces 82 | - **Example Code**: Working examples of providers and server 83 | - **Command-Line Help**: Documentation for command-line options 84 | 85 | ## Future Extensions 86 | 87 | The platform design allows for several future extensions: 88 | 89 | - **Dynamic Loading**: Support for loading providers from shared libraries 90 | - **Remote Providers**: Distributed providers running on different machines 91 | - **Authentication**: Provider and tool authentication system 92 | - **Monitoring**: Metrics and monitoring for tool executions 93 | - **Admin Interface**: Web-based administration console 94 | 95 | ## Conclusion 96 | 97 | The Cortex platform provides a flexible and extensible foundation for building MCP servers that can interact with various backend services through a standardized protocol. The clean separation between the core platform and specific tool implementations allows for independent evolution and ensures maintainability and scalability. -------------------------------------------------------------------------------- /agent-sdk-test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreePeak/cortex/64e47c1d4a73c1fe9fc2491c4ad49b9cd8c0aeb2/agent-sdk-test -------------------------------------------------------------------------------- /array-parameter-test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreePeak/cortex/64e47c1d4a73c1fe9fc2491c4ad49b9cd8c0aeb2/array-parameter-test -------------------------------------------------------------------------------- /directory-structure.md: -------------------------------------------------------------------------------- 1 | # Directory Structure 2 | 3 | The cortex is organized following clean architecture principles and GoLang best practices. 4 | 5 | ## Directory Overview 6 | 7 | ``` 8 | cortex/ 9 | ├── bin/ # Compiled binaries 10 | ├── cmd/ # Example MCP server applications 11 | │ ├── echo-sse-server/ # SSE-based example server 12 | │ ├── echo-stdio-server/ # StdIO-based example server 13 | │ └── multi-protocol-server/ # Server that supports multiple transport methods 14 | ├── examples/ # Example code snippets and use cases 15 | ├── internal/ # Private implementation details (not exposed to users) 16 | │ ├── builder/ # Internal builder implementation 17 | │ ├── domain/ # Core domain models and interfaces 18 | │ ├── infrastructure/ # Implementation of core interfaces 19 | │ ├── interfaces/ # Transport adapters (stdio, rest) 20 | │ └── usecases/ # Business logic and use cases 21 | └── pkg/ # Public API (exposed to users) 22 | ├── builder/ # Public builder pattern for server construction 23 | ├── server/ # Public server implementation 24 | ├── tools/ # Utilities for creating MCP tools 25 | └── types/ # Shared types and interfaces 26 | ``` 27 | 28 | ## Public API (pkg/) 29 | 30 | The `pkg/` directory contains all publicly exposed APIs that users of the SDK should interact with: 31 | 32 | - **pkg/builder/**: Builder pattern implementation for creating MCP servers. 33 | - **pkg/server/**: Core server implementation with support for different transports. 34 | - **pkg/tools/**: Helper functions for creating and configuring MCP tools. 35 | - **pkg/types/**: Core data structures and interfaces shared across the SDK. 36 | 37 | ## Private Implementation (internal/) 38 | 39 | The `internal/` directory contains implementation details that are not part of the public API: 40 | 41 | - **internal/domain/**: Core domain models, interfaces, and business logic. 42 | - **internal/infrastructure/**: Implementation of domain interfaces. 43 | - **internal/interfaces/**: Transport adapters for different protocols. 44 | - **internal/usecases/**: Application business rules and use cases. 45 | 46 | ## Examples and Applications 47 | 48 | - **cmd/**: Complete server applications that showcase different use cases. 49 | - **examples/**: Code snippets that demonstrate specific features of the SDK. 50 | 51 | ## Usage Guidelines 52 | 53 | 1. **For library consumers**: 54 | - Import only from the `pkg/` directory. 55 | - Use the builder pattern from `pkg/builder/` to create servers. 56 | - Use types from `pkg/types/` for interface parameters. 57 | - Use helpers from `pkg/tools/` to create tools with parameters. 58 | 59 | 2. **For library developers**: 60 | - Maintain clean separation between internal and public APIs. 61 | - Ensure backwards compatibility for the public API. 62 | - Add adapters between internal and public types to allow for future changes. 63 | 64 | ## Architectural Principles 65 | 66 | The SDK follows clean architecture principles: 67 | 68 | 1. **Dependency Rule**: Inner layers don't depend on outer layers. 69 | - Domain doesn't depend on infrastructure. 70 | - Use cases depend only on domain. 71 | 72 | 2. **Adapter Pattern**: Adapters connect external interfaces to internal logic. 73 | - Transport adapters (stdio, rest) abstract communication protocols. 74 | - Repository adapters abstract data storage. 75 | 76 | 3. **Dependency Injection**: Dependencies are provided from outside. 77 | - Services are constructed with their dependencies. 78 | - This makes testing easier and components more reusable. 79 | 80 | 4. **Interface Segregation**: Small, focused interfaces. 81 | - Repositories have specific purposes. 82 | - Handlers deal with specific types of messages. 83 | 84 | These principles make the SDK maintainable, testable, and adaptable to changes in requirements or external systems. -------------------------------------------------------------------------------- /docs/cortex-client-interaction-flow.md: -------------------------------------------------------------------------------- 1 | # Cortex MCP Server: Client Interaction Flow 2 | 3 | This document describes how the Cortex MCP server interacts with client applications such as Cursor IDE and Claude Desktop. 4 | 5 | ## Architecture Overview 6 | 7 | ```mermaid 8 | flowchart TD 9 | Client["Client Application\n(Cursor IDE / Claude Desktop)"] 10 | MCPServer["Cortex MCP Server"] 11 | ToolReg["Tool Registry\n(plugin.Registry)"] 12 | ToolHandlers["Tool Handlers\n(ToolHandler)"] 13 | NotifSystem["Notification System"] 14 | ToolProviders["Tool Providers\n(plugin.Provider)"] 15 | ExtServices["External Services"] 16 | Database["Database\n(via Provider)"] 17 | 18 | Client -- "JSON-RPC over HTTP" --> MCPServer 19 | MCPServer -- "Server-Sent Events (SSE)" --> Client 20 | 21 | MCPServer <--> ToolReg 22 | MCPServer <--> ToolHandlers 23 | MCPServer <--> NotifSystem 24 | 25 | ToolReg <--> ToolProviders 26 | ToolProviders <--> ExtServices 27 | ToolProviders <--> Database 28 | 29 | subgraph "Core Server Components" 30 | MCPServer 31 | ToolReg 32 | ToolHandlers 33 | NotifSystem 34 | end 35 | 36 | subgraph "External Integrations" 37 | ToolProviders 38 | ExtServices 39 | Database 40 | end 41 | ``` 42 | 43 | ## Connection Establishment 44 | 45 | 1. **Client Initiates Connection**: 46 | - The client connects to the Cortex MCP server via HTTP 47 | - Supported transport protocols: 48 | * HTTP API (REST) 49 | * Server-Sent Events (SSE) for real-time updates 50 | * Standard I/O (for command-line integration) 51 | 52 | 2. **Session Registration**: 53 | - Server assigns a unique session ID to the client 54 | - Client's user agent is recorded 55 | - A notification channel is established for server-to-client communication 56 | 57 | ## Communication Protocol (JSON-RPC) 58 | 59 | ### Client-to-Server Communication 60 | 61 | Clients send JSON-RPC messages to the server's message endpoint: 62 | 63 | ```json 64 | { 65 | "jsonrpc": "2.0", 66 | "id": "request-123", 67 | "method": "tools/call", 68 | "params": { 69 | "name": "tool_name", 70 | "parameters": { /* tool-specific parameters */ } 71 | } 72 | } 73 | ``` 74 | 75 | ### Server-to-Client Communication 76 | 77 | Server responds with JSON-RPC responses: 78 | 79 | ```json 80 | { 81 | "jsonrpc": "2.0", 82 | "id": "request-123", 83 | "result": { /* tool-specific response */ } 84 | } 85 | ``` 86 | 87 | For errors: 88 | 89 | ```json 90 | { 91 | "jsonrpc": "2.0", 92 | "id": "request-123", 93 | "error": { 94 | "code": -32000, 95 | "message": "Error message" 96 | } 97 | } 98 | ``` 99 | 100 | ### Real-time Updates via SSE 101 | 102 | - Server sends notifications using Server-Sent Events (SSE) 103 | - Notifications follow JSON-RPC format but without an ID field 104 | - Used for status updates, progress reports, etc. 105 | 106 | ## Tool Execution Flow 107 | 108 | ```mermaid 109 | sequenceDiagram 110 | participant Client 111 | participant Server as Cortex MCP Server 112 | participant Handler as Tool Handler 113 | participant Provider as Tool Provider 114 | 115 | Client->>Server: Connect & Register Session 116 | Server->>Client: Session ID & Endpoints 117 | 118 | Client->>Server: Tool Call Request 119 | Server->>Server: Validate Parameters 120 | 121 | alt Direct Tool 122 | Server->>Handler: Execute Tool 123 | Handler->>Server: Tool Result 124 | else Provider-based Tool 125 | Server->>Provider: Execute Tool 126 | Provider->>Provider: Process Request 127 | Provider->>Server: Tool Result 128 | end 129 | 130 | Server->>Client: JSON-RPC Response 131 | 132 | loop Real-time Updates 133 | Server->>Client: SSE Notifications 134 | end 135 | 136 | Client->>Server: Disconnect 137 | Server->>Server: Cleanup Session 138 | ``` 139 | 140 | ## Integration Modes 141 | 142 | 1. **Standalone Server**: 143 | - Server runs independently and listens on a port 144 | - Provides HTTP API and SSE endpoints 145 | 146 | 2. **Embedded Mode**: 147 | - Server is embedded within another application (e.g., PocketBase) 148 | - Uses HTTP adapter for integration with existing HTTP servers 149 | 150 | 3. **STDIO Mode**: 151 | - Server communicates via standard input/output 152 | - Useful for CLI tools and scripted interactions 153 | 154 | ## Key Components 155 | 156 | ```mermaid 157 | classDiagram 158 | class MCPServer { 159 | +AddTool() 160 | +RegisterProvider() 161 | +ServeHTTP() 162 | +ServeStdio() 163 | +ExecuteTool() 164 | +RegisterSession() 165 | } 166 | 167 | class SSEServer { 168 | +handleSSE() 169 | +handleMessage() 170 | +SendEventToSession() 171 | +BroadcastEvent() 172 | } 173 | 174 | class NotificationSender { 175 | +RegisterSession() 176 | +UnregisterSession() 177 | +SendNotification() 178 | +BroadcastNotification() 179 | } 180 | 181 | class ConnectionPool { 182 | +Add() 183 | +Remove() 184 | +Get() 185 | +Broadcast() 186 | } 187 | 188 | class ToolProvider { 189 | +GetTools() 190 | +ExecuteTool() 191 | } 192 | 193 | MCPServer --> SSEServer 194 | MCPServer --> NotificationSender 195 | SSEServer --> ConnectionPool 196 | MCPServer --> ToolProvider 197 | 198 | note for MCPServer "Core server implementation" 199 | note for SSEServer "Real-time communication" 200 | note for NotificationSender "Client notifications" 201 | note for ConnectionPool "Client session management" 202 | note for ToolProvider "Dynamic tool registration" 203 | ``` 204 | 205 | This architecture enables clients like Cursor IDE or Claude Desktop to interact with tools and services provided by the Cortex server, maintaining persistent connections for real-time updates. -------------------------------------------------------------------------------- /docs/embedding.md: -------------------------------------------------------------------------------- 1 | # Embedding Cortex in External Servers 2 | 3 | This document outlines how to embed Cortex functionality into an existing server application like PocketBase. 4 | 5 | ## Overview 6 | 7 | Cortex is designed to be either used as a standalone server or embedded within another application. When embedded, Cortex can provide MCP (Model Context Protocol) capabilities to your existing server without requiring a separate process. 8 | 9 | ## Integration Methods 10 | 11 | There are several ways to integrate Cortex with your existing application: 12 | 13 | 1. **Middleware Integration** - For web frameworks that support middleware, Cortex can be added as middleware to handle MCP requests. 14 | 2. **Route Handler Integration** - For frameworks with custom routing, Cortex handlers can be registered with your existing router. 15 | 3. **Plugin System Integration** - For applications with plugin capabilities, Cortex can be packaged as a plugin. 16 | 17 | ## Implementation Tasks 18 | 19 | To support embedding Cortex into external servers, we need to complete the following tasks: 20 | 21 | 1. Create an `Embeddable` interface that defines the integration points 22 | 2. Implement HTTP handler adapter for easy integration with HTTP servers 23 | 3. Create middleware adapters for common Go web frameworks 24 | 4. Develop a dedicated PocketBase plugin implementation 25 | 5. Add examples of embedding Cortex in different server types 26 | 6. Create documentation with step-by-step integration guides 27 | 7. Implement testing utilities for embedded integrations 28 | 29 | ## PocketBase Integration 30 | 31 | [PocketBase](https://github.com/pocketbase/pocketbase) is a good example of an application where Cortex can be embedded. PocketBase provides an API and database server with authentication. By embedding Cortex, PocketBase applications can gain MCP capabilities without running a separate server. 32 | 33 | To integrate Cortex with PocketBase, we'll implement a plugin that: 34 | 35 | 1. Registers as a PocketBase plugin 36 | 2. Exposes Cortex tools and resources through PocketBase's HTTP router 37 | 3. Shares the application context with Cortex 38 | 4. Provides authentication between PocketBase and Cortex 39 | 40 | ## Next Steps 41 | 42 | The following sections outline the specific tasks to implement embedding support in Cortex, starting with a minimal viable integration approach and building toward more sophisticated integration options. -------------------------------------------------------------------------------- /docs/implementation-plan.md: -------------------------------------------------------------------------------- 1 | # Cortex Embedding Implementation Plan 2 | 3 | This document provides a detailed plan for implementing embedding support in Cortex. Each task is designed to be small and independently verifiable, following the principles of test-driven development. 4 | 5 | ## Task 1: Create Embeddable Interface 6 | 7 | **Description**: Define an `Embeddable` interface that provides the core integration points for embedding Cortex in other applications. 8 | 9 | **Steps**: 10 | 1. Create the interface in `pkg/server/embeddable.go` 11 | 2. Define methods for creating handlers, adding tools, and accessing server information 12 | 3. Write tests for the interface 13 | 4. Update the MCPServer to implement this interface 14 | 15 | **Acceptance Criteria**: 16 | - Interface defined with clear method signatures 17 | - Tests pass for the interface implementation 18 | - Documentation provided for the interface 19 | 20 | ## Task 2: Implement HTTP Handler Adapter 21 | 22 | **Description**: Create an adapter that exposes Cortex functionality as an HTTP handler that can be used with any Go HTTP server. 23 | 24 | **Steps**: 25 | 1. Create `pkg/server/http_adapter.go` 26 | 2. Implement `ToHTTPHandler()` method that converts an Embeddable to an http.Handler 27 | 3. Implement SSE (Server-Sent Events) support 28 | 4. Write tests for the handler adapter 29 | 5. Create example usage in the documentation 30 | 31 | **Acceptance Criteria**: 32 | - HTTP handler properly handles MCP requests 33 | - SSE events are properly emitted 34 | - Tests pass for the handler implementation 35 | - Documentation provides clear usage examples 36 | 37 | ## Task 3: Create Standard Middleware Adapters 38 | 39 | **Description**: Implement middleware adapters for common Go web frameworks to simplify integration. 40 | 41 | **Steps**: 42 | 1. Create `pkg/server/middleware.go` 43 | 2. Implement adapter for standard net/http middleware 44 | 3. Write tests for the middleware adapter 45 | 4. Document usage examples 46 | 47 | **Acceptance Criteria**: 48 | - Middleware adapter correctly passes requests to Cortex 49 | - Tests pass for the middleware implementation 50 | - Documentation provides clear usage examples 51 | 52 | ## Task 4: Implement PocketBase Plugin 53 | 54 | **Description**: Create a plugin for PocketBase that exposes Cortex functionality. 55 | 56 | **Steps**: 57 | 1. Create `pkg/integration/pocketbase/plugin.go` 58 | 2. Implement the PocketBase Plugin interface 59 | 3. Create a factory function for creating the plugin 60 | 4. Write tests for the plugin 61 | 5. Document how to use the plugin 62 | 63 | **Acceptance Criteria**: 64 | - Plugin correctly registers with PocketBase 65 | - Cortex tools and resources are accessible through PocketBase 66 | - Tests pass for the plugin implementation 67 | - Documentation provides clear usage instructions 68 | 69 | ## Task 5: Add Basic Embedding Examples 70 | 71 | **Description**: Create examples of embedding Cortex in different server types. 72 | 73 | **Steps**: 74 | 1. Create example for embedding in standard Go HTTP server 75 | 2. Create example for embedding in a PocketBase application 76 | 3. Ensure examples are well-documented 77 | 4. Test examples to verify functionality 78 | 79 | **Acceptance Criteria**: 80 | - Examples work as expected 81 | - Documentation clearly explains the examples 82 | - Examples are simple enough to serve as starting points 83 | 84 | ## Task 6: Implement Authentication Support 85 | 86 | **Description**: Add support for using the host application's authentication in Cortex. 87 | 88 | **Steps**: 89 | 1. Create `pkg/server/auth.go` 90 | 2. Implement authentication adapter interface 91 | 3. Create default implementation that uses HTTP headers 92 | 4. Write tests for authentication support 93 | 5. Document authentication integration 94 | 95 | **Acceptance Criteria**: 96 | - Authentication adapter works with host application credentials 97 | - Tests pass for authentication implementation 98 | - Documentation provides clear integration examples 99 | 100 | ## Task 7: Create Comprehensive Documentation 101 | 102 | **Description**: Create comprehensive documentation for embedding Cortex. 103 | 104 | **Steps**: 105 | 1. Update main README to mention embedding support 106 | 2. Create detailed guide for PocketBase integration 107 | 3. Add API documentation for embedding interfaces 108 | 4. Include troubleshooting section 109 | 5. Create tutorial for embedding in a simple application 110 | 111 | **Acceptance Criteria**: 112 | - Documentation is clear and comprehensive 113 | - Examples demonstrate real-world use cases 114 | - API documentation is complete and accurate 115 | 116 | ## Implementation Order 117 | 118 | These tasks should be implemented in the following order: 119 | 120 | 1. Task 1: Create Embeddable Interface 121 | 2. Task 2: Implement HTTP Handler Adapter 122 | 3. Task 3: Create Standard Middleware Adapters 123 | 4. Task 5: Add Basic Embedding Examples 124 | 5. Task 4: Implement PocketBase Plugin 125 | 6. Task 6: Implement Authentication Support 126 | 7. Task 7: Create Comprehensive Documentation 127 | 128 | This ordering allows each task to build on the previous ones, with early tasks providing the foundation for later ones. -------------------------------------------------------------------------------- /docs/implementation-status.md: -------------------------------------------------------------------------------- 1 | # Cortex Embedding Implementation Status 2 | 3 | This document tracks the progress of implementing embedding support in Cortex. 4 | 5 | ## Completed Tasks 6 | 7 | 1. **Task 1: Create Embeddable Interface** 8 | - Created the `Embeddable` interface in `pkg/server/embeddable.go` 9 | - Updated `MCPServer` to implement this interface 10 | - Wrote tests in `pkg/server/embeddable_test.go` 11 | - All tests passing 12 | 13 | 2. **Task 2: Implement HTTP Handler Adapter** 14 | - Created `pkg/server/http_adapter.go` with the `HTTPAdapter` type 15 | - Implemented path-based routing for HTTP integration 16 | - Wrote tests in `pkg/server/http_adapter_test.go` 17 | - All tests passing 18 | 19 | 3. **Task 4: Implement PocketBase Plugin** 20 | - Created the plugin in `pkg/integration/pocketbase/plugin.go` 21 | - Implemented configuration options with functional options pattern 22 | - Added methods for HTTP integration 23 | - Created tests in `pkg/integration/pocketbase/plugin_test.go` 24 | 25 | 4. **Task 5: Add Basic Embedding Examples** 26 | - Created example for embedding in PocketBase: `examples/integration/pocketbase` 27 | - Implemented a mock PocketBase server to demonstrate integration 28 | - Added detailed documentation for the example 29 | 30 | ## Pending Tasks 31 | 32 | 1. **Task 3: Create Standard Middleware Adapters** 33 | - Implement adapters for common Go web frameworks 34 | 35 | 2. **Task 5: Complete Additional Examples** 36 | - Create example for embedding in standard Go HTTP server 37 | 38 | 3. **Task 6: Implement Authentication Support** 39 | - Add support for using the host application's authentication in Cortex 40 | 41 | 4. **Task 7: Create Comprehensive Documentation** 42 | - Update main README to mention embedding support 43 | - Expand PocketBase integration guide 44 | - Complete API documentation 45 | - Create troubleshooting section 46 | 47 | ## Next Steps 48 | 49 | The immediate next steps are: 50 | 51 | 1. Implement a standard middleware adapter for Go's http.Handler 52 | 2. Create an example of using Cortex embedded in a standard HTTP server 53 | 3. Add authentication support to the PocketBase plugin 54 | 55 | ## Quality Assurance 56 | 57 | - All implemented code passes unit tests 58 | - All implemented code passes linting checks with golangci-lint 59 | - Code follows Go best practices and project conventions -------------------------------------------------------------------------------- /docs/pocketbase-integration.md: -------------------------------------------------------------------------------- 1 | # Integrating Cortex with PocketBase 2 | 3 | This guide explains how to integrate Cortex into a [PocketBase](https://github.com/pocketbase/pocketbase) application to provide MCP (Model Context Protocol) capabilities. 4 | 5 | ## Overview 6 | 7 | PocketBase is an open-source Go backend that provides a database, auth system, realtime subscriptions, and admin UI. By embedding Cortex into PocketBase, you can add MCP capabilities to your PocketBase application, allowing it to: 8 | 9 | 1. Expose tools for LLMs to call 10 | 2. Provide resources for LLMs to access 11 | 3. Support Model Context Protocol (MCP) communication 12 | 13 | ## Integration Methods 14 | 15 | There are two primary ways to integrate Cortex with PocketBase: 16 | 17 | 1. **Plugin Integration**: Use Cortex's PocketBase plugin for seamless integration 18 | 2. **Manual Integration**: Manually add Cortex handlers to your PocketBase router 19 | 20 | This guide focuses on the plugin integration approach, which is simpler and recommended for most users. 21 | 22 | ## Plugin Integration 23 | 24 | ### Prerequisites 25 | 26 | - Go 1.20 or later 27 | - A PocketBase application 28 | 29 | ### Step 1: Add Dependencies 30 | 31 | Add Cortex to your PocketBase application's `go.mod` file: 32 | 33 | ```bash 34 | go get github.com/FreePeak/cortex 35 | ``` 36 | 37 | ### Step 2: Create a Cortex Plugin 38 | 39 | In your PocketBase application, create a new file for the Cortex plugin integration: 40 | 41 | ```go 42 | // cortex_plugin.go 43 | package main 44 | 45 | import ( 46 | "log" 47 | "os" 48 | 49 | "github.com/FreePeak/cortex/pkg/integration/pocketbase" 50 | "github.com/FreePeak/cortex/pkg/tools" 51 | ) 52 | 53 | func createCortexPlugin(app *pocketbase.PocketBase) { 54 | // Create a logger for the plugin 55 | logger := log.New(os.Stderr, "[cortex] ", log.LstdFlags) 56 | 57 | // Create the plugin with desired options 58 | plugin := pocketbase.NewCortexPlugin( 59 | pocketbase.WithLogger(logger), 60 | pocketbase.WithBasePath("/api/mcp"), 61 | pocketbase.WithName("My PocketBase MCP Server"), 62 | pocketbase.WithVersion("1.0.0"), 63 | ) 64 | 65 | // Register tools 66 | echoTool := tools.NewTool("echo", 67 | tools.WithDescription("Echoes back the input message"), 68 | tools.WithString("message", 69 | tools.Description("The message to echo back"), 70 | tools.Required(), 71 | ), 72 | ) 73 | 74 | // Register the tool with a handler 75 | plugin.AddTool(echoTool, func(ctx context.Context, request pocketbase.ToolCallRequest) (interface{}, error) { 76 | message := request.Parameters["message"].(string) 77 | return map[string]interface{}{ 78 | "content": []map[string]interface{}{ 79 | { 80 | "type": "text", 81 | "text": message, 82 | }, 83 | }, 84 | }, nil 85 | }) 86 | 87 | // Register the plugin with PocketBase 88 | app.RegisterPlugin(plugin) 89 | } 90 | ``` 91 | 92 | ### Step 3: Use the Plugin in Your PocketBase App 93 | 94 | Update your main application to use the Cortex plugin: 95 | 96 | ```go 97 | // main.go 98 | package main 99 | 100 | import ( 101 | "log" 102 | 103 | "github.com/pocketbase/pocketbase" 104 | "github.com/pocketbase/pocketbase/core" 105 | ) 106 | 107 | func main() { 108 | app := pocketbase.New() 109 | 110 | // Register hooks and other PocketBase configuration... 111 | 112 | // Register the Cortex plugin 113 | createCortexPlugin(app) 114 | 115 | // Start the PocketBase app 116 | if err := app.Start(); err != nil { 117 | log.Fatal(err) 118 | } 119 | } 120 | ``` 121 | 122 | ### Step 4: Access the MCP Server 123 | 124 | Once your application is running, the MCP server will be available at the configured base path (default: `/api/mcp`). You can interact with it using MCP clients like the official [MCP JS SDK](https://github.com/Model-Context-Protocol/mcp-js). 125 | 126 | ## Authentication and Authorization 127 | 128 | By default, the Cortex plugin uses PocketBase's authentication system. You can configure authentication requirements in the plugin options: 129 | 130 | ```go 131 | plugin := pocketbase.NewCortexPlugin( 132 | // ...other options... 133 | pocketbase.WithAuth(pocketbase.AuthOptions{ 134 | Required: true, // Require authentication for all MCP endpoints 135 | AdminOnly: false, // Allow regular users (not just admins) 136 | AllowedRoles: []string{"api"}, // Only allow users with the "api" role 137 | }), 138 | ) 139 | ``` 140 | 141 | ## Adding Resources from PocketBase Collections 142 | 143 | You can expose PocketBase collections as MCP resources: 144 | 145 | ```go 146 | plugin.AddCollectionResource("tasks", "Sample Tasks", "List of example tasks") 147 | ``` 148 | 149 | This will make the collection available as an MCP resource that LLMs can access. 150 | 151 | ## Advanced Configuration 152 | 153 | For more advanced configuration options, see the full API documentation in the Cortex repository. 154 | 155 | ## Example Application 156 | 157 | A complete example application that demonstrates Cortex integration with PocketBase is available in the `examples/integration/pocketbase` directory of the Cortex repository. 158 | 159 | ## Troubleshooting 160 | 161 | If you encounter issues with the integration: 162 | 163 | 1. Check the logs for error messages 164 | 2. Verify your PocketBase application is running correctly 165 | 3. Ensure the Cortex plugin is registered properly 166 | 4. Confirm that your tools are registered with the correct handlers 167 | 168 | For more help, see the troubleshooting section in the main Cortex documentation. -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Cortex Examples 2 | 3 | This directory contains example applications that demonstrate how to use the Cortex MCP server platform in various scenarios. 4 | 5 | ## Server Examples 6 | 7 | These examples showcase different server configurations and communication protocols: 8 | 9 | - **stdio-server**: A simple stdio-based MCP server example 10 | - **sse-server**: An HTTP/SSE-based MCP server example 11 | - **multi-protocol**: A server that supports both stdio and HTTP protocols 12 | 13 | ## Integration Examples 14 | 15 | The `integration` directory contains examples of embedding Cortex in other applications: 16 | 17 | - **pocketbase**: Demonstrates how to integrate Cortex with PocketBase 18 | 19 | ## Tool Provider Examples 20 | 21 | The `providers` directory contains example tool providers that can be used with the Cortex platform: 22 | 23 | - **weather**: Weather forecast tool provider 24 | - **database**: Simple key-value store tool provider 25 | 26 | ## Running the Examples 27 | 28 | ### Stdio Server 29 | 30 | ```bash 31 | go run examples/stdio-server/main.go 32 | ``` 33 | 34 | This will start a stdio server that accepts JSON-RPC requests from standard input. 35 | 36 | ### SSE Server 37 | 38 | ```bash 39 | go run examples/sse-server/main.go 40 | ``` 41 | 42 | This will start an HTTP server on port 8080 that accepts MCP requests via Server-Sent Events (SSE). 43 | 44 | ### Multi-Protocol Server 45 | 46 | ```bash 47 | # Run with stdio protocol 48 | go run examples/multi-protocol/main.go -protocol stdio 49 | 50 | # Run with HTTP protocol 51 | go run examples/multi-protocol/main.go -protocol http -address localhost:8080 52 | ``` 53 | 54 | This example shows how to create a server that can switch between stdio and HTTP protocols. 55 | 56 | ### PocketBase Integration 57 | 58 | ```bash 59 | go run examples/integration/pocketbase/main.go 60 | ``` 61 | 62 | This example demonstrates how to embed Cortex in a PocketBase application, using a mock PocketBase server for simplicity. -------------------------------------------------------------------------------- /examples/agent-sdk-test/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/FreePeak/cortex/pkg/server" 13 | "github.com/FreePeak/cortex/pkg/tools" 14 | ) 15 | 16 | func main() { 17 | // Create a logger that writes to stderr 18 | logger := log.New(os.Stderr, "[agent-sdk-test] ", log.LstdFlags) 19 | 20 | // Create the server 21 | mcpServer := server.NewMCPServer("Agent SDK Test", "1.0.0", logger) 22 | 23 | // Configure HTTP address 24 | mcpServer.SetAddress(":9095") 25 | 26 | // Create a tool with array parameter (compatible with OpenAI Agent SDK) 27 | queryTool := tools.NewTool("query_database", 28 | tools.WithDescription("Execute SQL query on a database"), 29 | tools.WithString("query", 30 | tools.Description("SQL query to execute"), 31 | tools.Required(), 32 | ), 33 | tools.WithArray("params", 34 | tools.Description("Query parameters"), 35 | tools.Items(map[string]interface{}{ 36 | "type": "string", 37 | }), 38 | ), 39 | ) 40 | 41 | // Add tool to the server 42 | ctx := context.Background() 43 | err := mcpServer.AddTool(ctx, queryTool, handleQuery) 44 | if err != nil { 45 | logger.Fatalf("Error adding tool: %v", err) 46 | } 47 | 48 | // Start HTTP server in a goroutine 49 | go func() { 50 | logger.Printf("Starting Agent SDK Test server on %s", mcpServer.GetAddress()) 51 | logger.Printf("Use the following URL in your OpenAI Agent SDK configuration: http://localhost:9095/sse") 52 | 53 | if err := mcpServer.ServeHTTP(); err != nil { 54 | logger.Fatalf("HTTP server error: %v", err) 55 | } 56 | }() 57 | 58 | // Wait for shutdown signal 59 | stop := make(chan os.Signal, 1) 60 | signal.Notify(stop, os.Interrupt, syscall.SIGTERM) 61 | <-stop 62 | 63 | // Shutdown gracefully 64 | logger.Println("Shutting down server...") 65 | shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 66 | defer cancel() 67 | 68 | if err := mcpServer.Shutdown(shutdownCtx); err != nil { 69 | logger.Fatalf("Server shutdown error: %v", err) 70 | } 71 | 72 | logger.Println("Server shutdown complete") 73 | } 74 | 75 | // Handler for the query tool 76 | func handleQuery(ctx context.Context, request server.ToolCallRequest) (interface{}, error) { 77 | // Extract the query parameter 78 | query, ok := request.Parameters["query"].(string) 79 | if !ok { 80 | return nil, fmt.Errorf("missing or invalid 'query' parameter") 81 | } 82 | 83 | // Get optional parameters 84 | var params []interface{} 85 | if paramsVal, ok := request.Parameters["params"]; ok { 86 | params, _ = paramsVal.([]interface{}) 87 | } 88 | 89 | // In a real implementation, you would execute the query with the parameters 90 | // For this example, we'll just return mock data 91 | 92 | // Log the request 93 | log.Printf("Query received: %s", query) 94 | log.Printf("Parameters: %v", params) 95 | 96 | // Return a mock response 97 | return map[string]interface{}{ 98 | "content": []map[string]interface{}{ 99 | { 100 | "type": "text", 101 | "text": fmt.Sprintf("Executed query: %s\nParameters: %v\n\nID\tName\tValue\n1\tItem1\t100\n2\tItem2\t200", query, params), 102 | }, 103 | }, 104 | }, nil 105 | } 106 | -------------------------------------------------------------------------------- /examples/array-parameter-test/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "os" 9 | 10 | "github.com/FreePeak/cortex/pkg/server" 11 | "github.com/FreePeak/cortex/pkg/tools" 12 | "github.com/FreePeak/cortex/pkg/types" 13 | ) 14 | 15 | func main() { 16 | // Create a logger that writes to stderr 17 | logger := log.New(os.Stderr, "[array-test] ", log.LstdFlags) 18 | 19 | // Create the server 20 | mcpServer := server.NewMCPServer("Array Parameter Test", "1.0.0", logger) 21 | 22 | // Create a tool with array parameter 23 | arrayTool := tools.NewTool("array_test", 24 | tools.WithDescription("Test tool with array parameter"), 25 | tools.WithArray("string_array", 26 | tools.Description("Array of strings"), 27 | tools.Required(), 28 | tools.Items(map[string]interface{}{ 29 | "type": "string", 30 | }), 31 | ), 32 | tools.WithArray("number_array", 33 | tools.Description("Array of numbers"), 34 | tools.Items(map[string]interface{}{ 35 | "type": "number", 36 | }), 37 | ), 38 | ) 39 | 40 | // Add the tool to the server 41 | ctx := context.Background() 42 | err := mcpServer.AddTool(ctx, arrayTool, handleArrayTest) 43 | if err != nil { 44 | logger.Fatalf("Error adding tool: %v", err) 45 | } 46 | 47 | // Print tool schema for debugging 48 | printToolSchema(arrayTool) 49 | 50 | // Write server status to stderr 51 | fmt.Fprintf(os.Stderr, "Starting Array Parameter Test Server...\n") 52 | fmt.Fprintf(os.Stderr, "Send JSON-RPC messages via stdin to interact with the server.\n") 53 | fmt.Fprintf(os.Stderr, `Try: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"array_test","parameters":{"string_array":["a","b","c"]}}}\n`) 54 | 55 | // Serve over stdio 56 | if err := mcpServer.ServeStdio(); err != nil { 57 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 58 | os.Exit(1) 59 | } 60 | } 61 | 62 | // Handler for the array test tool 63 | func handleArrayTest(ctx context.Context, request server.ToolCallRequest) (interface{}, error) { 64 | // Extract the string array parameter 65 | stringArray, ok := request.Parameters["string_array"].([]interface{}) 66 | if !ok { 67 | return nil, fmt.Errorf("missing or invalid 'string_array' parameter") 68 | } 69 | 70 | // Get the optional number array parameter 71 | var numberArray []interface{} 72 | if val, ok := request.Parameters["number_array"]; ok { 73 | numberArray, _ = val.([]interface{}) 74 | } 75 | 76 | // Return the arrays in the response 77 | return map[string]interface{}{ 78 | "content": []map[string]interface{}{ 79 | { 80 | "type": "text", 81 | "text": fmt.Sprintf("Received string array: %v\nReceived number array: %v", stringArray, numberArray), 82 | }, 83 | }, 84 | }, nil 85 | } 86 | 87 | // Print the tool schema 88 | func printToolSchema(tool *types.Tool) { 89 | schema := map[string]interface{}{ 90 | "type": "object", 91 | "properties": make(map[string]interface{}), 92 | } 93 | 94 | for _, param := range tool.Parameters { 95 | paramSchema := map[string]interface{}{ 96 | "type": param.Type, 97 | "description": param.Description, 98 | } 99 | 100 | if param.Type == "array" && param.Items != nil { 101 | paramSchema["items"] = param.Items 102 | } 103 | 104 | schema["properties"].(map[string]interface{})[param.Name] = paramSchema 105 | } 106 | 107 | schemaJSON, _ := json.MarshalIndent(schema, "", " ") 108 | fmt.Fprintf(os.Stderr, "Tool schema:\n%s\n", schemaJSON) 109 | } 110 | -------------------------------------------------------------------------------- /examples/integration/pocketbase/README.md: -------------------------------------------------------------------------------- 1 | # PocketBase Integration Example 2 | 3 | This example demonstrates how to integrate Cortex with [PocketBase](https://github.com/pocketbase/pocketbase), allowing you to add MCP capabilities to your PocketBase applications. 4 | 5 | ## Overview 6 | 7 | In this example, we're using a mock PocketBase server since we don't want to add PocketBase as a dependency to the Cortex repository. In a real application, you would replace the mock with the actual PocketBase library. 8 | 9 | The example shows: 10 | 11 | 1. How to create a Cortex plugin for PocketBase 12 | 2. How to configure the plugin with custom options 13 | 3. How to add tools to the plugin 14 | 4. How to register the plugin with PocketBase 15 | 16 | ## Running the Example 17 | 18 | To run this example: 19 | 20 | ```bash 21 | go run examples/integration/pocketbase/main.go 22 | ``` 23 | 24 | This will start a mock PocketBase server with the Cortex plugin registered at the `/api/mcp` path. 25 | 26 | By default, the server runs on port 8080. If this port is already in use, you can specify a different port: 27 | 28 | ```bash 29 | go run examples/integration/pocketbase/main.go --port 8090 30 | ``` 31 | 32 | You can also specify a custom data directory for PocketBase: 33 | 34 | ```bash 35 | go run examples/integration/pocketbase/main.go --data ./my_data_dir 36 | ``` 37 | 38 | ### Using the Run Script 39 | 40 | For convenience, you can also use the included run script: 41 | 42 | ```bash 43 | # Run with default settings 44 | ./run.sh 45 | 46 | # Run with custom port 47 | ./run.sh --port 8090 48 | 49 | # Run with custom data directory 50 | ./run.sh --data ./my_data_dir 51 | 52 | # Kill any existing processes on the port before starting 53 | ./run.sh --kill 54 | 55 | # Show help 56 | ./run.sh --help 57 | ``` 58 | 59 | ### Troubleshooting Connection Issues 60 | 61 | If you encounter connection issues: 62 | 63 | 1. **Port in use**: If port 8080 is already in use, use the `--port` flag to specify a different port 64 | 2. **404 errors with SSE connections**: Ensure your client is using the correct URL path (`/api/mcp/sse`) 65 | 3. **CORS issues**: The server is configured with permissive CORS headers, but some browsers might still block requests. Consider using a CORS extension or proxy if needed 66 | 67 | To kill any processes using port 8080, you can use the included script: 68 | 69 | ```bash 70 | # Make the script executable 71 | chmod +x kill-port.sh 72 | 73 | # Kill processes on the default port (8080) 74 | ./kill-port.sh 75 | 76 | # Or specify a different port 77 | ./kill-port.sh 8090 78 | ``` 79 | 80 | ### Connecting with Cursor Editor 81 | 82 | To connect the Cursor editor to this example: 83 | 84 | 1. Run the server with: `go run examples/integration/pocketbase/main.go` 85 | 2. In Cursor, open the MCP settings 86 | 3. Set the MCP URL to: `http://localhost:8080/api/mcp` (or with your custom port) 87 | 4. Test the connection using the Echo tool 88 | 89 | ### Testing the Connection 90 | 91 | You can test the server using the included test clients: 92 | 93 | #### Browser Test (HTML UI) 94 | 95 | 1. Start the server with: `go run examples/integration/pocketbase/main.go` 96 | 2. Open the `test-client.html` file in your browser 97 | 3. Use the interactive UI to test different aspects of the connection 98 | 99 | #### JavaScript Console Test 100 | 101 | 1. Start the server with: `go run examples/integration/pocketbase/main.go` 102 | 2. Open `test-client.js` in a browser console or import it in your HTML 103 | 3. Check the console for test results 104 | 105 | #### Node.js Test 106 | 107 | 1. Start the server with: `go run examples/integration/pocketbase/main.go` 108 | 2. Run the test client: `node test-client.js` 109 | 110 | All test clients will verify: 111 | - Basic connectivity to the server 112 | - Echo tool functionality 113 | - Weather tool functionality 114 | - SSE connection capability 115 | 116 | If you're using a custom port, make sure to update the port settings in the test clients. 117 | 118 | ## Key Components 119 | 120 | ### CortexPlugin 121 | 122 | The `CortexPlugin` is the main integration point between Cortex and PocketBase. It: 123 | 124 | - Wraps an `MCPServer` to provide MCP functionality 125 | - Exposes methods for adding tools and registering with PocketBase 126 | - Provides HTTP handlers that can be registered with PocketBase 127 | 128 | ### Tool Handlers 129 | 130 | The example includes two tool handlers: 131 | 132 | 1. **Echo Tool**: Echoes back the input message 133 | 2. **Weather Tool**: Simulates getting a weather forecast for a location 134 | 135 | ### PocketBase Integration 136 | 137 | In a real PocketBase application, you would integrate Cortex like this: 138 | 139 | ```go 140 | import ( 141 | "github.com/pocketbase/pocketbase" 142 | "github.com/pocketbase/pocketbase/core" 143 | "github.com/FreePeak/cortex/pkg/integration/pocketbase" 144 | ) 145 | 146 | func main() { 147 | app := pocketbase.New() 148 | 149 | // Initialize Cortex plugin 150 | plugin := pocketbase.NewCortexPlugin( 151 | pocketbase.WithName("PocketBase MCP Server"), 152 | pocketbase.WithVersion("1.0.0"), 153 | pocketbase.WithBasePath("/api/mcp"), 154 | ) 155 | 156 | // Add tools to the plugin 157 | // ... 158 | 159 | // Register the plugin with PocketBase 160 | app.OnBeforeServe().Add(func(e *core.ServeEvent) error { 161 | // Get the Cortex HTTP handler 162 | handler := plugin.GetHTTPHandler() 163 | 164 | // Register it with PocketBase's router 165 | e.Router.ANY("/api/mcp/*", handler) 166 | 167 | return nil 168 | }) 169 | 170 | // Start the PocketBase app 171 | if err := app.Start(); err != nil { 172 | panic(err) 173 | } 174 | } 175 | ``` 176 | 177 | ## Learn More 178 | 179 | For more detailed documentation on integrating Cortex with PocketBase, see the [PocketBase Integration Guide](../../../docs/pocketbase-integration.md). -------------------------------------------------------------------------------- /examples/integration/pocketbase/kill-port.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script to kill processes using a specific port 3 | 4 | PORT=${1:-8080} 5 | 6 | echo "Attempting to find and kill processes using port $PORT..." 7 | 8 | # For Mac/Linux 9 | if command -v lsof >/dev/null 2>&1; then 10 | PIDS=$(lsof -ti:$PORT) 11 | if [ -n "$PIDS" ]; then 12 | echo "Found processes: $PIDS" 13 | kill -9 $PIDS 14 | echo "Processes killed" 15 | else 16 | echo "No processes found using port $PORT" 17 | fi 18 | # For Windows 19 | elif command -v netstat >/dev/null 2>&1 && command -v taskkill >/dev/null 2>&1; then 20 | PID=$(netstat -ano | grep ":$PORT" | awk '{print $5}' | head -n 1) 21 | if [ -n "$PID" ]; then 22 | echo "Found process: $PID" 23 | taskkill /F /PID $PID 24 | echo "Process killed" 25 | else 26 | echo "No processes found using port $PORT" 27 | fi 28 | else 29 | echo "Could not find appropriate command to check for processes" 30 | exit 1 31 | fi 32 | 33 | echo "Done!" -------------------------------------------------------------------------------- /examples/integration/pocketbase/pocketbase: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreePeak/cortex/64e47c1d4a73c1fe9fc2491c4ad49b9cd8c0aeb2/examples/integration/pocketbase/pocketbase -------------------------------------------------------------------------------- /examples/integration/pocketbase/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Script to run the PocketBase integration example with various configurations 3 | 4 | # Default values 5 | PORT=8080 6 | DATA_DIR="./pb_data" 7 | KILL_EXISTING=false 8 | 9 | # Parse arguments 10 | while [[ $# -gt 0 ]]; do 11 | case $1 in 12 | -p|--port) 13 | PORT="$2" 14 | shift 2 15 | ;; 16 | -d|--data) 17 | DATA_DIR="$2" 18 | shift 2 19 | ;; 20 | -k|--kill) 21 | KILL_EXISTING=true 22 | shift 23 | ;; 24 | -h|--help) 25 | echo "Usage: $0 [options]" 26 | echo "Options:" 27 | echo " -p, --port PORT Server port (default: 8080)" 28 | echo " -d, --data DIR Data directory (default: ./pb_data)" 29 | echo " -k, --kill Kill any existing processes using the specified port" 30 | echo " -h, --help Show this help message" 31 | exit 0 32 | ;; 33 | *) 34 | echo "Unknown option: $1" 35 | exit 1 36 | ;; 37 | esac 38 | done 39 | 40 | # Kill existing processes if requested 41 | if [ "$KILL_EXISTING" = true ]; then 42 | echo "Checking for processes using port $PORT..." 43 | if command -v lsof >/dev/null 2>&1; then 44 | PIDS=$(lsof -ti:$PORT) 45 | if [ -n "$PIDS" ]; then 46 | echo "Killing processes: $PIDS" 47 | kill -9 $PIDS 48 | else 49 | echo "No processes found using port $PORT" 50 | fi 51 | else 52 | echo "lsof command not found, cannot check for existing processes" 53 | fi 54 | fi 55 | 56 | # Run the server 57 | echo "Starting PocketBase integration example on port $PORT with data directory $DATA_DIR" 58 | go run main.go --port $PORT --data $DATA_DIR 59 | 60 | # Exit with the same status as the server 61 | exit $? -------------------------------------------------------------------------------- /examples/integration/pocketbase/test-client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple test client for the PocketBase MCP integration 3 | * Run this in a browser or with Node.js to test the connection 4 | */ 5 | 6 | const PORT = 8080; // Change this if you're using a custom port 7 | const BASE_URL = `http://localhost:${PORT}/api/mcp`; 8 | 9 | // Echo test function 10 | async function testEcho() { 11 | console.log('Testing echo tool...'); 12 | 13 | try { 14 | const response = await fetch(`${BASE_URL}/run`, { 15 | method: 'POST', 16 | headers: { 17 | 'Content-Type': 'application/json', 18 | }, 19 | body: JSON.stringify({ 20 | tool: 'echo', 21 | args: { 22 | message: 'Hello from test client!' 23 | } 24 | }), 25 | }); 26 | 27 | if (!response.ok) { 28 | throw new Error(`HTTP error: ${response.status}`); 29 | } 30 | 31 | const data = await response.json(); 32 | console.log('Echo response:', data); 33 | return data; 34 | } catch (error) { 35 | console.error('Echo test failed:', error); 36 | throw error; 37 | } 38 | } 39 | 40 | // Weather test function 41 | async function testWeather() { 42 | console.log('Testing weather tool...'); 43 | 44 | try { 45 | const response = await fetch(`${BASE_URL}/run`, { 46 | method: 'POST', 47 | headers: { 48 | 'Content-Type': 'application/json', 49 | }, 50 | body: JSON.stringify({ 51 | tool: 'weather', 52 | args: { 53 | location: 'San Francisco' 54 | } 55 | }), 56 | }); 57 | 58 | if (!response.ok) { 59 | throw new Error(`HTTP error: ${response.status}`); 60 | } 61 | 62 | const data = await response.json(); 63 | console.log('Weather response:', data); 64 | return data; 65 | } catch (error) { 66 | console.error('Weather test failed:', error); 67 | throw error; 68 | } 69 | } 70 | 71 | // Test SSE connection 72 | function testSSE() { 73 | console.log('Testing SSE connection...'); 74 | 75 | return new Promise((resolve, reject) => { 76 | const eventSource = new EventSource(`${BASE_URL}/sse`); 77 | 78 | eventSource.onopen = () => { 79 | console.log('SSE connection established'); 80 | }; 81 | 82 | eventSource.onmessage = (event) => { 83 | console.log('SSE message received:', event.data); 84 | eventSource.close(); 85 | resolve(event.data); 86 | }; 87 | 88 | eventSource.onerror = (error) => { 89 | console.error('SSE connection error:', error); 90 | eventSource.close(); 91 | reject(error); 92 | }; 93 | 94 | // Close the connection after 5 seconds if no messages received 95 | setTimeout(() => { 96 | console.log('SSE test timeout - closing connection'); 97 | eventSource.close(); 98 | resolve('No messages received, but connection was established'); 99 | }, 5000); 100 | }); 101 | } 102 | 103 | // Run all tests 104 | async function runTests() { 105 | try { 106 | console.log(`Testing connection to ${BASE_URL}`); 107 | 108 | // Test basic connectivity first 109 | const response = await fetch(`${BASE_URL}/tools`); 110 | if (!response.ok) { 111 | throw new Error(`Failed to connect to server: ${response.status}`); 112 | } 113 | const tools = await response.json(); 114 | console.log('Available tools:', tools); 115 | 116 | // Run the tests 117 | await testEcho(); 118 | await testWeather(); 119 | await testSSE(); 120 | 121 | console.log('All tests completed successfully!'); 122 | } catch (error) { 123 | console.error('Tests failed:', error); 124 | } 125 | } 126 | 127 | // Run the tests when executed 128 | runTests(); 129 | 130 | // Export for use in other contexts 131 | if (typeof module !== 'undefined') { 132 | module.exports = { 133 | testEcho, 134 | testWeather, 135 | testSSE, 136 | runTests 137 | }; 138 | } -------------------------------------------------------------------------------- /examples/multi-protocol/README.md: -------------------------------------------------------------------------------- 1 | # Multi-Protocol MCP Server Example 2 | 3 | This example demonstrates an MCP server that can communicate over either stdio or HTTP, depending on the configuration. It integrates with external tool providers to showcase the plugin architecture. 4 | 5 | ## Features 6 | 7 | - Supports both stdio and HTTP communication protocols 8 | - Integrates with external tool providers (weather and database) 9 | - Command-line configurable settings 10 | - Graceful shutdown support 11 | 12 | ## Running the Example 13 | 14 | ### Stdio Mode 15 | 16 | ```bash 17 | go run examples/multi-protocol/main.go -protocol stdio 18 | ``` 19 | 20 | ### HTTP Mode 21 | 22 | ```bash 23 | go run examples/multi-protocol/main.go -protocol http -address localhost:8080 24 | ``` 25 | 26 | ## Command-Line Options 27 | 28 | - `-protocol`: Communication protocol (stdio or http, default: stdio) 29 | - `-address`: HTTP server address when using http protocol (default: localhost:8080) 30 | 31 | ## Available Tools 32 | 33 | This example integrates with two tool providers: 34 | 35 | ### Weather Provider 36 | 37 | - `weather`: Gets today's weather forecast for a location 38 | - `forecast`: Gets a multi-day weather forecast for a location 39 | 40 | Example JSON-RPC request (stdio): 41 | 42 | ```json 43 | { 44 | "jsonrpc": "2.0", 45 | "id": 1, 46 | "method": "tools/call", 47 | "params": { 48 | "name": "weather", 49 | "parameters": { 50 | "location": "New York" 51 | } 52 | } 53 | } 54 | ``` 55 | 56 | ### Database Provider 57 | 58 | - `db.get`: Gets a value from the database by key 59 | - `db.set`: Sets a value in the database 60 | - `db.delete`: Deletes a value from the database 61 | - `db.keys`: Lists all keys in the database 62 | 63 | Example JSON-RPC request (stdio): 64 | 65 | ```json 66 | { 67 | "jsonrpc": "2.0", 68 | "id": 2, 69 | "method": "tools/call", 70 | "params": { 71 | "name": "db.set", 72 | "parameters": { 73 | "key": "user1", 74 | "value": { 75 | "name": "John", 76 | "age": 30 77 | } 78 | } 79 | } 80 | } 81 | ``` 82 | 83 | ## Code Structure 84 | 85 | - `main.go`: Sets up the server and registers the tool providers 86 | - `examples/providers/weather`: Weather provider implementation 87 | - `examples/providers/database`: Database provider implementation 88 | 89 | ## Implementation Details 90 | 91 | The server is implemented using the Cortex MCP server platform. It uses the `server.NewMCPServer` function to create a server instance and registers tool providers using the `RegisterProvider` method. The server is configured to use either stdio or HTTP based on the command-line options. -------------------------------------------------------------------------------- /examples/multi-protocol/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | 12 | "github.com/FreePeak/cortex/examples/providers/database" 13 | "github.com/FreePeak/cortex/examples/providers/weather" 14 | "github.com/FreePeak/cortex/pkg/server" 15 | ) 16 | 17 | func main() { 18 | // Parse command-line flags 19 | protocol := flag.String("protocol", "stdio", "Communication protocol (stdio or http)") 20 | address := flag.String("address", "localhost:8080", "HTTP server address (when using http protocol)") 21 | flag.Parse() 22 | 23 | // Create a logger 24 | logger := log.New(os.Stdout, "[cortex] ", log.LstdFlags) 25 | 26 | // Create the MCP server 27 | mcpServer := server.NewMCPServer("Multi-Protocol Server", "1.0.0", logger) 28 | 29 | // Set HTTP address if using HTTP protocol 30 | if *protocol == "http" { 31 | mcpServer.SetAddress(*address) 32 | } 33 | 34 | // Create the weather provider 35 | weatherProvider, err := weather.NewWeatherProvider(logger) 36 | if err != nil { 37 | logger.Fatalf("Failed to create weather provider: %v", err) 38 | } 39 | 40 | // Create the database provider 41 | dbProvider, err := database.NewDBProvider(logger) 42 | if err != nil { 43 | logger.Fatalf("Failed to create database provider: %v", err) 44 | } 45 | 46 | // Register providers with the server 47 | ctx := context.Background() 48 | err = mcpServer.RegisterProvider(ctx, weatherProvider) 49 | if err != nil { 50 | logger.Fatalf("Failed to register weather provider: %v", err) 51 | } 52 | 53 | err = mcpServer.RegisterProvider(ctx, dbProvider) 54 | if err != nil { 55 | logger.Fatalf("Failed to register database provider: %v", err) 56 | } 57 | 58 | // Set up graceful shutdown 59 | stop := make(chan os.Signal, 1) 60 | signal.Notify(stop, os.Interrupt, syscall.SIGTERM) 61 | 62 | // Start the server based on the specified protocol 63 | if *protocol == "stdio" { 64 | // Print server ready message for stdio 65 | fmt.Println("Server ready. You can now send JSON-RPC requests via stdin.") 66 | fmt.Println("Example weather tool request:") 67 | fmt.Println(`{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"weather","parameters":{"location":"New York"}}}`) 68 | fmt.Println("Example database set tool request:") 69 | fmt.Println(`{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"db.set","parameters":{"key":"user1","value":{"name":"John","age":30}}}}`) 70 | 71 | // Start the stdio server (this will block until terminated) 72 | go func() { 73 | if err := mcpServer.ServeStdio(); err != nil { 74 | logger.Fatalf("Error serving stdio: %v", err) 75 | } 76 | }() 77 | } else if *protocol == "http" { 78 | // Print server ready message for HTTP 79 | fmt.Printf("HTTP server starting on %s\n", *address) 80 | fmt.Println("You can query tools using HTTP POST requests to /tools/call") 81 | 82 | // Start the HTTP server (this will block until terminated) 83 | go func() { 84 | if err := mcpServer.ServeHTTP(); err != nil { 85 | logger.Fatalf("Error serving HTTP: %v", err) 86 | } 87 | }() 88 | } else { 89 | logger.Fatalf("Unknown protocol: %s (must be 'stdio' or 'http')", *protocol) 90 | } 91 | 92 | // Wait for shutdown signal 93 | <-stop 94 | logger.Println("Shutting down server...") 95 | 96 | // Shutdown the server gracefully 97 | if *protocol == "http" { 98 | shutdownCtx := context.Background() 99 | if err := mcpServer.Shutdown(shutdownCtx); err != nil { 100 | logger.Fatalf("Error shutting down server: %v", err) 101 | } 102 | } 103 | 104 | logger.Println("Server stopped") 105 | } 106 | -------------------------------------------------------------------------------- /examples/providers/README.md: -------------------------------------------------------------------------------- 1 | # Cortex Example Providers 2 | 3 | This directory contains example implementations of Cortex tool providers that demonstrate how to integrate with the Cortex MCP server platform. These examples are designed to showcase the flexibility of the plugin architecture. 4 | 5 | ## Weather Provider 6 | 7 | The Weather Provider (`weather` directory) demonstrates integrating a simple weather service: 8 | 9 | - **weather tool**: Gets today's weather forecast for a location 10 | - **forecast tool**: Gets a multi-day weather forecast for a location 11 | 12 | Example usage (via JSON-RPC): 13 | 14 | ```json 15 | { 16 | "jsonrpc": "2.0", 17 | "id": 1, 18 | "method": "tools/call", 19 | "params": { 20 | "name": "weather", 21 | "parameters": { 22 | "location": "New York" 23 | } 24 | } 25 | } 26 | ``` 27 | 28 | ```json 29 | { 30 | "jsonrpc": "2.0", 31 | "id": 2, 32 | "method": "tools/call", 33 | "params": { 34 | "name": "forecast", 35 | "parameters": { 36 | "location": "San Francisco", 37 | "days": 5 38 | } 39 | } 40 | } 41 | ``` 42 | 43 | ## Database Provider 44 | 45 | The Database Provider (`database` directory) demonstrates implementing a simple in-memory key-value store: 46 | 47 | - **db.get tool**: Gets a value from the database by key 48 | - **db.set tool**: Sets a value in the database 49 | - **db.delete tool**: Deletes a value from the database 50 | - **db.keys tool**: Lists all keys in the database 51 | 52 | Example usage (via JSON-RPC): 53 | 54 | ```json 55 | { 56 | "jsonrpc": "2.0", 57 | "id": 1, 58 | "method": "tools/call", 59 | "params": { 60 | "name": "db.set", 61 | "parameters": { 62 | "key": "user1", 63 | "value": { 64 | "name": "John Doe", 65 | "email": "john@example.com", 66 | "age": 30 67 | } 68 | } 69 | } 70 | } 71 | ``` 72 | 73 | ```json 74 | { 75 | "jsonrpc": "2.0", 76 | "id": 2, 77 | "method": "tools/call", 78 | "params": { 79 | "name": "db.get", 80 | "parameters": { 81 | "key": "user1" 82 | } 83 | } 84 | } 85 | ``` 86 | 87 | ## Running Example Providers 88 | 89 | The example providers are integrated into the flexible server example. To run the server with these providers: 90 | 91 | ```bash 92 | # Run with stdio protocol 93 | ./run-flexible-stdio.sh 94 | 95 | # Run with HTTP protocol 96 | ./run-flexible-http.sh 97 | ``` 98 | 99 | ## Creating Your Own Provider 100 | 101 | To create your own provider, you can use these examples as templates. Here are the basic steps: 102 | 103 | 1. Define a struct that embeds `plugin.BaseProvider` 104 | 2. Create a constructor that initializes the provider and registers tools 105 | 3. Implement tool handler functions 106 | 4. Register your provider with the flexible MCP server 107 | 108 | For a complete guide, see the README in the `pkg/plugin` directory. 109 | 110 | ## Best Practices 111 | 112 | When creating your own providers, follow these best practices: 113 | 114 | 1. Use descriptive names for your provider and tools 115 | 2. Define clear parameter names and descriptions 116 | 3. Validate all input parameters 117 | 4. Return informative error messages 118 | 5. Follow the MCP protocol response format 119 | 6. Use context for cancellation and timeouts 120 | 7. Implement proper logging 121 | 122 | ## Extending These Examples 123 | 124 | These examples are intentionally simple to demonstrate the core concepts. In a real-world implementation, you might want to: 125 | 126 | - Connect to actual external services 127 | - Implement caching for better performance 128 | - Add authentication and authorization 129 | - Implement rate limiting 130 | - Add more sophisticated error handling 131 | - Include monitoring and metrics -------------------------------------------------------------------------------- /examples/providers/weather/provider.go: -------------------------------------------------------------------------------- 1 | // Package weather provides a weather service provider for the Cortex platform. 2 | package weather 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "log" 8 | "math/rand" 9 | "time" 10 | 11 | "github.com/FreePeak/cortex/pkg/plugin" 12 | "github.com/FreePeak/cortex/pkg/tools" 13 | "github.com/FreePeak/cortex/pkg/types" 14 | ) 15 | 16 | // WeatherProvider implements the plugin.Provider interface for weather forecasts. 17 | type WeatherProvider struct { 18 | *plugin.BaseProvider 19 | } 20 | 21 | // NewWeatherProvider creates a new weather provider. 22 | func NewWeatherProvider(logger *log.Logger) (*WeatherProvider, error) { 23 | // Create provider info 24 | info := plugin.ProviderInfo{ 25 | ID: "cortex-weather-provider", 26 | Name: "Weather Provider", 27 | Version: "1.0.0", 28 | Description: "A provider for getting weather forecasts", 29 | Author: "Cortex Team", 30 | URL: "https://github.com/FreePeak/cortex", 31 | } 32 | 33 | // Create base provider 34 | baseProvider := plugin.NewBaseProvider(info, logger) 35 | 36 | // Create weather provider 37 | provider := &WeatherProvider{ 38 | BaseProvider: baseProvider, 39 | } 40 | 41 | // Register weather tool 42 | weatherTool := tools.NewTool("weather", 43 | tools.WithDescription("Gets today's weather forecast"), 44 | tools.WithString("location", 45 | tools.Description("The location to get weather for"), 46 | tools.Required(), 47 | ), 48 | ) 49 | 50 | // Register the tool with the provider 51 | err := provider.RegisterTool(weatherTool, provider.handleWeather) 52 | if err != nil { 53 | return nil, fmt.Errorf("failed to register weather tool: %w", err) 54 | } 55 | 56 | // Register forecast tool 57 | forecastTool := tools.NewTool("forecast", 58 | tools.WithDescription("Gets a multi-day weather forecast"), 59 | tools.WithString("location", 60 | tools.Description("The location to get forecast for"), 61 | tools.Required(), 62 | ), 63 | tools.WithNumber("days", 64 | tools.Description("Number of days to forecast (1-7)"), 65 | tools.Required(), 66 | ), 67 | ) 68 | 69 | // Register the tool with the provider 70 | err = provider.RegisterTool(forecastTool, provider.handleForecast) 71 | if err != nil { 72 | return nil, fmt.Errorf("failed to register forecast tool: %w", err) 73 | } 74 | 75 | return provider, nil 76 | } 77 | 78 | // handleWeather handles the weather tool requests. 79 | func (p *WeatherProvider) handleWeather(ctx context.Context, params map[string]interface{}, session *types.ClientSession) (interface{}, error) { 80 | // Extract the location parameter 81 | location, ok := params["location"].(string) 82 | if !ok { 83 | return nil, fmt.Errorf("missing or invalid 'location' parameter") 84 | } 85 | 86 | // Generate random weather data for testing 87 | conditions := []string{"Sunny", "Partly Cloudy", "Cloudy", "Rainy", "Thunderstorms", "Snowy", "Foggy", "Windy"} 88 | tempF := rand.Intn(50) + 30 // Random temperature between 30°F and 80°F 89 | tempC := (tempF - 32) * 5 / 9 90 | humidity := rand.Intn(60) + 30 // Random humidity between 30% and 90% 91 | windSpeed := rand.Intn(20) + 5 // Random wind speed between 5-25mph 92 | 93 | // Select a random condition 94 | condition := conditions[rand.Intn(len(conditions))] 95 | 96 | // Format today's date 97 | today := time.Now().Format("Monday, January 2, 2006") 98 | 99 | // Format the weather response 100 | weatherInfo := fmt.Sprintf("Weather for %s on %s:\n"+ 101 | "Condition: %s\n"+ 102 | "Temperature: %d°F (%d°C)\n"+ 103 | "Humidity: %d%%\n"+ 104 | "Wind Speed: %d mph", 105 | location, today, condition, tempF, tempC, humidity, windSpeed) 106 | 107 | // Return the weather response in the format expected by the MCP protocol 108 | return map[string]interface{}{ 109 | "content": []map[string]interface{}{ 110 | { 111 | "type": "text", 112 | "text": weatherInfo, 113 | }, 114 | }, 115 | }, nil 116 | } 117 | 118 | // handleForecast handles the forecast tool requests. 119 | func (p *WeatherProvider) handleForecast(ctx context.Context, params map[string]interface{}, session *types.ClientSession) (interface{}, error) { 120 | // Extract the location parameter 121 | location, ok := params["location"].(string) 122 | if !ok { 123 | return nil, fmt.Errorf("missing or invalid 'location' parameter") 124 | } 125 | 126 | // Extract the days parameter 127 | daysFloat, ok := params["days"].(float64) 128 | if !ok { 129 | return nil, fmt.Errorf("missing or invalid 'days' parameter") 130 | } 131 | 132 | days := int(daysFloat) 133 | if days < 1 || days > 7 { 134 | return nil, fmt.Errorf("days must be between 1 and 7") 135 | } 136 | 137 | // Weather conditions 138 | conditions := []string{"Sunny", "Partly Cloudy", "Cloudy", "Rainy", "Thunderstorms", "Snowy", "Foggy", "Windy"} 139 | 140 | // Generate forecast for each day 141 | var forecastText string 142 | forecastText = fmt.Sprintf("Weather Forecast for %s:\n\n", location) 143 | 144 | for i := 0; i < days; i++ { 145 | // Get date for this forecast day 146 | forecastDate := time.Now().AddDate(0, 0, i).Format("Monday, January 2") 147 | 148 | // Generate random weather data 149 | condition := conditions[rand.Intn(len(conditions))] 150 | tempF := rand.Intn(50) + 30 151 | tempC := (tempF - 32) * 5 / 9 152 | 153 | // Add to forecast text 154 | forecastText += fmt.Sprintf("%s:\n", forecastDate) 155 | forecastText += fmt.Sprintf(" Condition: %s\n", condition) 156 | forecastText += fmt.Sprintf(" Temperature: %d°F (%d°C)\n\n", tempF, tempC) 157 | } 158 | 159 | // Return the forecast response in the format expected by the MCP protocol 160 | return map[string]interface{}{ 161 | "content": []map[string]interface{}{ 162 | { 163 | "type": "text", 164 | "text": forecastText, 165 | }, 166 | }, 167 | }, nil 168 | } 169 | -------------------------------------------------------------------------------- /examples/sse-server/README.md: -------------------------------------------------------------------------------- 1 | # SSE MCP Server Example 2 | 3 | This example demonstrates an MCP server that communicates over HTTP using Server-Sent Events (SSE). It provides two basic tools: an echo tool and a weather forecast tool. 4 | 5 | ## Features 6 | 7 | - Communicates using HTTP and Server-Sent Events 8 | - Provides an echo tool that reflects messages back to the client 9 | - Provides a weather tool that generates random weather forecasts 10 | - Supports graceful shutdown 11 | 12 | ## Running the Example 13 | 14 | ```bash 15 | go run examples/sse-server/main.go 16 | ``` 17 | 18 | By default, the server listens on port 8080. 19 | 20 | ## Usage 21 | 22 | Once the server is running, you can connect to it using any HTTP client that supports SSE. For example, you can use curl: 23 | 24 | ```bash 25 | curl -X POST http://localhost:8080/tools/call -H "Content-Type: application/json" -d '{"name":"echo","parameters":{"message":"Hello, World!"}}' 26 | ``` 27 | 28 | ### Integration with Cursor 29 | 30 | You can connect to this server from Cursor by going to Settings > Extensions > Model Context Protocol and entering `http://localhost:8080` as the server URL. 31 | 32 | ### Available Tools 33 | 34 | #### Echo Tool 35 | 36 | Example HTTP request: 37 | 38 | ```bash 39 | curl -X POST http://localhost:8080/tools/call -H "Content-Type: application/json" -d '{"name":"echo","parameters":{"message":"Hello, World!"}}' 40 | ``` 41 | 42 | #### Weather Tool 43 | 44 | Example HTTP request: 45 | 46 | ```bash 47 | curl -X POST http://localhost:8080/tools/call -H "Content-Type: application/json" -d '{"name":"weather","parameters":{"location":"New York"}}' 48 | ``` 49 | 50 | ## Code Structure 51 | 52 | - `main.go`: Sets up the server and registers the tools 53 | - Tool handlers: 54 | - `handleEcho`: Processes echo tool requests 55 | - `handleWeather`: Processes weather tool requests 56 | 57 | ## Implementation Details 58 | 59 | The server is implemented using the Cortex MCP server platform. It uses the `server.NewMCPServer` function to create a server instance and registers tools using the `AddTool` method. The server is configured to listen on HTTP and uses graceful shutdown to ensure all connections are properly closed when the server is terminated. -------------------------------------------------------------------------------- /examples/sse-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "math/rand" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/FreePeak/cortex/pkg/server" 14 | "github.com/FreePeak/cortex/pkg/tools" 15 | ) 16 | 17 | const ( 18 | serverName = "Example SSE MCP Server" 19 | serverVersion = "1.0.0" 20 | serverAddr = ":8080" 21 | shutdownTimeout = 10 * time.Second 22 | shutdownGraceful = 2 * time.Second 23 | ) 24 | 25 | func main() { 26 | // Create a logger 27 | logger := log.New(os.Stdout, "[cortex-sse] ", log.LstdFlags) 28 | 29 | // Create a new server using the SDK 30 | mcpServer := server.NewMCPServer(serverName, serverVersion, logger) 31 | 32 | // Set the server address 33 | mcpServer.SetAddress(serverAddr) 34 | 35 | // Create tools with the fluent API 36 | echoTool := tools.NewTool("echo", 37 | tools.WithDescription("Echoes back the input message"), 38 | tools.WithString("message", 39 | tools.Description("The message to echo back"), 40 | tools.Required(), 41 | ), 42 | ) 43 | 44 | // Create the weather tool 45 | weatherTool := tools.NewTool("weather", 46 | tools.WithDescription("Gets today's weather forecast"), 47 | tools.WithString("location", 48 | tools.Description("The location to get weather for"), 49 | tools.Required(), 50 | ), 51 | ) 52 | 53 | // Add tools with handlers 54 | ctx := context.Background() 55 | err := mcpServer.AddTool(ctx, echoTool, handleEcho) 56 | if err != nil { 57 | logger.Fatalf("Error adding echo tool: %v", err) 58 | } 59 | 60 | err = mcpServer.AddTool(ctx, weatherTool, handleWeather) 61 | if err != nil { 62 | logger.Fatalf("Error adding weather tool: %v", err) 63 | } 64 | 65 | // Handle graceful shutdown 66 | shutdown := make(chan os.Signal, 1) 67 | signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) 68 | 69 | // Start server in a goroutine 70 | go func() { 71 | fmt.Printf("Server is running on %s\n", serverAddr) 72 | fmt.Printf("You can connect to this server from Cursor by going to Settings > Extensions > Model Context Protocol and entering 'http://localhost%s' as the server URL.\n", serverAddr) 73 | fmt.Println("Available tools: echo, weather") 74 | fmt.Println("Press Ctrl+C to stop") 75 | 76 | // Use the SDK's built-in HTTP server functionality 77 | if err := mcpServer.ServeHTTP(); err != nil { 78 | logger.Fatalf("Server failed to start: %v", err) 79 | } 80 | }() 81 | 82 | // Wait for shutdown signal 83 | <-shutdown 84 | fmt.Println("Shutting down server...") 85 | 86 | // Create a context with timeout for shutdown 87 | ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) 88 | defer cancel() 89 | 90 | // Shutdown server 91 | if err := mcpServer.Shutdown(ctx); err != nil { 92 | logger.Fatalf("Server forced to shutdown: %v", err) 93 | } 94 | 95 | // Small delay to allow final cleanup 96 | time.Sleep(shutdownGraceful) 97 | fmt.Println("Server stopped gracefully") 98 | } 99 | 100 | // Echo tool handler 101 | func handleEcho(ctx context.Context, request server.ToolCallRequest) (interface{}, error) { 102 | // Extract the message parameter 103 | message, ok := request.Parameters["message"].(string) 104 | if !ok { 105 | return nil, fmt.Errorf("missing or invalid 'message' parameter") 106 | } 107 | 108 | // Return the echo response in the format expected by the MCP protocol 109 | return map[string]interface{}{ 110 | "content": []map[string]interface{}{ 111 | { 112 | "type": "text", 113 | "text": message, 114 | }, 115 | }, 116 | }, nil 117 | } 118 | 119 | // Weather tool handler 120 | func handleWeather(ctx context.Context, request server.ToolCallRequest) (interface{}, error) { 121 | // Extract the location parameter 122 | location, ok := request.Parameters["location"].(string) 123 | if !ok { 124 | return nil, fmt.Errorf("missing or invalid 'location' parameter") 125 | } 126 | 127 | // Generate random weather data for testing 128 | conditions := []string{"Sunny", "Partly Cloudy", "Cloudy", "Rainy", "Thunderstorms", "Snowy", "Foggy", "Windy"} 129 | tempF := rand.Intn(50) + 30 // Random temperature between 30°F and 80°F 130 | tempC := (tempF - 32) * 5 / 9 131 | humidity := rand.Intn(60) + 30 // Random humidity between 30% and 90% 132 | windSpeed := rand.Intn(20) + 5 // Random wind speed between 5-25mph 133 | 134 | // Select a random condition 135 | condition := conditions[rand.Intn(len(conditions))] 136 | 137 | // Format today's date 138 | today := time.Now().Format("Monday, January 2, 2006") 139 | 140 | // Format the weather response 141 | weatherInfo := fmt.Sprintf("Weather for %s on %s:\n"+ 142 | "Condition: %s\n"+ 143 | "Temperature: %d°F (%d°C)\n"+ 144 | "Humidity: %d%%\n"+ 145 | "Wind Speed: %d mph", 146 | location, today, condition, tempF, tempC, humidity, windSpeed) 147 | 148 | // Return the weather response in the format expected by the MCP protocol 149 | return map[string]interface{}{ 150 | "content": []map[string]interface{}{ 151 | { 152 | "type": "text", 153 | "text": weatherInfo, 154 | }, 155 | }, 156 | }, nil 157 | } 158 | -------------------------------------------------------------------------------- /examples/stdio-server/README.md: -------------------------------------------------------------------------------- 1 | # Stdio MCP Server Example 2 | 3 | This example demonstrates a simple MCP server that communicates over standard input/output. It provides two basic tools: an echo tool and a weather forecast tool. 4 | 5 | ## Features 6 | 7 | - Communicates using the JSON-RPC protocol over standard I/O 8 | - Provides an echo tool that reflects messages back to the client 9 | - Provides a weather tool that generates random weather forecasts 10 | 11 | ## Running the Example 12 | 13 | ```bash 14 | go run examples/stdio-server/main.go 15 | ``` 16 | 17 | ## Usage 18 | 19 | Once the server is running, you can send JSON-RPC requests via standard input. Here are some example requests: 20 | 21 | ### Echo Tool 22 | 23 | ```json 24 | {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo","parameters":{"message":"Hello, World!"}}} 25 | ``` 26 | 27 | This will echo back the message "Hello, World!" with a timestamp. 28 | 29 | ### Weather Tool 30 | 31 | ```json 32 | {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"weather","parameters":{"location":"New York"}}} 33 | ``` 34 | 35 | This will return a random weather forecast for New York. 36 | 37 | ## Code Structure 38 | 39 | - `main.go`: Sets up the server and registers the tools 40 | - Tool handlers: 41 | - `handleEcho`: Processes echo tool requests 42 | - `handleWeather`: Processes weather tool requests 43 | 44 | ## Implementation Details 45 | 46 | The server is implemented using the Cortex MCP server platform. It uses the `server.NewMCPServer` function to create a server instance and registers tools using the `AddTool` method. Tool handlers are implemented as functions that take a context and a `ToolCallRequest` and return a response. -------------------------------------------------------------------------------- /examples/stdio-server/logs/cortex-20250401-003240.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreePeak/cortex/64e47c1d4a73c1fe9fc2491c4ad49b9cd8c0aeb2/examples/stdio-server/logs/cortex-20250401-003240.log -------------------------------------------------------------------------------- /examples/stdio-server/logs/cortex-20250401-003528.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreePeak/cortex/64e47c1d4a73c1fe9fc2491c4ad49b9cd8c0aeb2/examples/stdio-server/logs/cortex-20250401-003528.log -------------------------------------------------------------------------------- /examples/stdio-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "math/rand" 8 | "os" 9 | "time" 10 | 11 | "github.com/FreePeak/cortex/pkg/server" 12 | "github.com/FreePeak/cortex/pkg/tools" 13 | ) 14 | 15 | // Record a timestamp for demo purposes 16 | func getTimestamp() string { 17 | return fmt.Sprintf("%d", time.Now().Unix()) 18 | } 19 | 20 | func main() { 21 | // Create a logger that writes to stderr instead of stdout 22 | // This is critical for STDIO servers as stdout must only contain JSON-RPC messages 23 | logger := log.New(os.Stderr, "[cortex-stdio] ", log.LstdFlags) 24 | 25 | // Create the server with name and version 26 | mcpServer := server.NewMCPServer("Cortex Stdio Server", "1.0.0", logger) 27 | 28 | // Initialize random seed 29 | rand.Seed(time.Now().UnixNano()) 30 | 31 | // Create the echo tool using the fluent API 32 | echoTool := tools.NewTool("echo", 33 | tools.WithDescription("Echoes back the input message"), 34 | tools.WithString("message", 35 | tools.Description("The message to echo back"), 36 | tools.Required(), 37 | ), 38 | ) 39 | 40 | // Create the weather tool 41 | weatherTool := tools.NewTool("weather", 42 | tools.WithDescription("Gets today's weather forecast"), 43 | tools.WithString("location", 44 | tools.Description("The location to get weather for"), 45 | tools.Required(), 46 | ), 47 | ) 48 | 49 | // Add the tools with handler functions 50 | ctx := context.Background() 51 | err := mcpServer.AddTool(ctx, echoTool, handleEcho) 52 | if err != nil { 53 | logger.Fatalf("Error adding echo tool: %v", err) 54 | } 55 | 56 | err = mcpServer.AddTool(ctx, weatherTool, handleWeather) 57 | if err != nil { 58 | logger.Fatalf("Error adding weather tool: %v", err) 59 | } 60 | 61 | // Write server status to stderr instead of stdout to maintain clean JSON protocol 62 | fmt.Fprintf(os.Stderr, "Server ready. The following tools are available:\n") 63 | fmt.Fprintf(os.Stderr, "- echo\n") 64 | fmt.Fprintf(os.Stderr, "- weather\n") 65 | 66 | // Start the STDIO server 67 | if err := mcpServer.ServeStdio(); err != nil { 68 | fmt.Fprintf(os.Stderr, "Error serving stdio: %v\n", err) 69 | os.Exit(1) 70 | } 71 | } 72 | 73 | // Echo tool handler 74 | func handleEcho(ctx context.Context, request server.ToolCallRequest) (interface{}, error) { 75 | // Log request details to stderr via the logger 76 | log.Printf("Handling echo tool call with name: %s", request.Name) 77 | 78 | // Extract the message parameter 79 | message, ok := request.Parameters["message"].(string) 80 | if !ok { 81 | return nil, fmt.Errorf("missing or invalid 'message' parameter") 82 | } 83 | 84 | // Add a timestamp to show we can process the message 85 | timestamp := getTimestamp() 86 | responseMessage := fmt.Sprintf("[%s] %s", timestamp, message) 87 | 88 | // Return the echo response in the format expected by the MCP protocol 89 | return map[string]interface{}{ 90 | "content": []map[string]interface{}{ 91 | { 92 | "type": "text", 93 | "text": responseMessage, 94 | }, 95 | }, 96 | }, nil 97 | } 98 | 99 | // Weather tool handler 100 | func handleWeather(ctx context.Context, request server.ToolCallRequest) (interface{}, error) { 101 | // Log request details to stderr via the logger 102 | log.Printf("Handling weather tool call with name: %s", request.Name) 103 | 104 | // Extract the location parameter 105 | location, ok := request.Parameters["location"].(string) 106 | if !ok { 107 | return nil, fmt.Errorf("missing or invalid 'location' parameter") 108 | } 109 | 110 | // Generate random weather data for testing 111 | conditions := []string{"Sunny", "Partly Cloudy", "Cloudy", "Rainy", "Thunderstorms", "Snowy", "Foggy", "Windy"} 112 | tempF := rand.Intn(50) + 30 // Random temperature between 30°F and 80°F 113 | tempC := (tempF - 32) * 5 / 9 114 | humidity := rand.Intn(60) + 30 // Random humidity between 30% and 90% 115 | windSpeed := rand.Intn(20) + 5 // Random wind speed between 5-25mph 116 | 117 | // Select a random condition 118 | condition := conditions[rand.Intn(len(conditions))] 119 | 120 | // Format today's date 121 | today := time.Now().Format("Monday, January 2, 2006") 122 | 123 | // Format the weather response 124 | weatherInfo := fmt.Sprintf("Weather for %s on %s:\n"+ 125 | "Condition: %s\n"+ 126 | "Temperature: %d°F (%d°C)\n"+ 127 | "Humidity: %d%%\n"+ 128 | "Wind Speed: %d mph", 129 | location, today, condition, tempF, tempC, humidity, windSpeed) 130 | 131 | // Return the weather response in the format expected by the MCP protocol 132 | return map[string]interface{}{ 133 | "content": []map[string]interface{}{ 134 | { 135 | "type": "text", 136 | "text": weatherInfo, 137 | }, 138 | }, 139 | }, nil 140 | } 141 | -------------------------------------------------------------------------------- /examples/stdio-server/test-stdio-server: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreePeak/cortex/64e47c1d4a73c1fe9fc2491c4ad49b9cd8c0aeb2/examples/stdio-server/test-stdio-server -------------------------------------------------------------------------------- /fix-imports.sh: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/FreePeak/cortex 2 | 3 | go 1.23 4 | 5 | toolchain go1.24.1 6 | 7 | require github.com/google/uuid v1.6.0 8 | 9 | require ( 10 | github.com/davecgh/go-spew v1.1.1 // indirect 11 | github.com/pmezard/go-difflib v1.0.0 // indirect 12 | github.com/stretchr/objx v0.5.2 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | 16 | require ( 17 | github.com/stretchr/testify v1.10.0 18 | go.uber.org/multierr v1.11.0 // indirect 19 | go.uber.org/zap v1.27.0 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 4 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 8 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 9 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 10 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 11 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 12 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 13 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 14 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 15 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 16 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 20 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 21 | -------------------------------------------------------------------------------- /internal/builder/serverbuilder.go: -------------------------------------------------------------------------------- 1 | // Package builder provides a builder for the MCP server. 2 | package builder 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/FreePeak/cortex/internal/domain" 8 | "github.com/FreePeak/cortex/internal/infrastructure/logging" 9 | "github.com/FreePeak/cortex/internal/infrastructure/server" 10 | "github.com/FreePeak/cortex/internal/interfaces/rest" 11 | "github.com/FreePeak/cortex/internal/interfaces/stdio" 12 | "github.com/FreePeak/cortex/internal/usecases" 13 | ) 14 | 15 | // ServerBuilder is a builder for creating an MCP server. 16 | type ServerBuilder struct { 17 | name string 18 | version string 19 | address string 20 | instructions string 21 | resourceRepo domain.ResourceRepository 22 | toolRepo domain.ToolRepository 23 | promptRepo domain.PromptRepository 24 | sessionRepo domain.SessionRepository 25 | notificationSender domain.NotificationSender 26 | 27 | // Maintain a single instance of the server service 28 | serverService *usecases.ServerService 29 | } 30 | 31 | // NewServerBuilder creates a new ServerBuilder. 32 | func NewServerBuilder() *ServerBuilder { 33 | // Create default repositories 34 | toolRepo := server.NewInMemoryToolRepository() 35 | resourceRepo := server.NewInMemoryResourceRepository() 36 | promptRepo := server.NewInMemoryPromptRepository() 37 | sessionRepo := server.NewInMemorySessionRepository() 38 | 39 | return &ServerBuilder{ 40 | name: "MCP Server", 41 | version: "1.0.0", 42 | address: ":8080", 43 | instructions: "MCP Server for AI tools and resources", 44 | resourceRepo: resourceRepo, 45 | toolRepo: toolRepo, 46 | promptRepo: promptRepo, 47 | sessionRepo: sessionRepo, 48 | serverService: nil, // Will be initialized when first needed 49 | } 50 | } 51 | 52 | // WithName sets the server name 53 | func (b *ServerBuilder) WithName(name string) *ServerBuilder { 54 | b.name = name 55 | return b 56 | } 57 | 58 | // WithVersion sets the server version 59 | func (b *ServerBuilder) WithVersion(version string) *ServerBuilder { 60 | b.version = version 61 | return b 62 | } 63 | 64 | // WithInstructions sets the server instructions 65 | func (b *ServerBuilder) WithInstructions(instructions string) *ServerBuilder { 66 | b.instructions = instructions 67 | return b 68 | } 69 | 70 | // WithAddress sets the server address 71 | func (b *ServerBuilder) WithAddress(address string) *ServerBuilder { 72 | b.address = address 73 | return b 74 | } 75 | 76 | // WithResourceRepository sets the resource repository 77 | func (b *ServerBuilder) WithResourceRepository(repo domain.ResourceRepository) *ServerBuilder { 78 | b.resourceRepo = repo 79 | return b 80 | } 81 | 82 | // WithToolRepository sets the tool repository 83 | func (b *ServerBuilder) WithToolRepository(repo domain.ToolRepository) *ServerBuilder { 84 | b.toolRepo = repo 85 | return b 86 | } 87 | 88 | // WithPromptRepository sets the prompt repository 89 | func (b *ServerBuilder) WithPromptRepository(repo domain.PromptRepository) *ServerBuilder { 90 | b.promptRepo = repo 91 | return b 92 | } 93 | 94 | // WithSessionRepository sets the session repository 95 | func (b *ServerBuilder) WithSessionRepository(repo domain.SessionRepository) *ServerBuilder { 96 | b.sessionRepo = repo 97 | return b 98 | } 99 | 100 | // WithNotificationSender sets the notification sender 101 | func (b *ServerBuilder) WithNotificationSender(sender domain.NotificationSender) *ServerBuilder { 102 | b.notificationSender = sender 103 | return b 104 | } 105 | 106 | // AddTool adds a tool to the server's tool repository 107 | func (b *ServerBuilder) AddTool(ctx context.Context, tool *domain.Tool) *ServerBuilder { 108 | if b.toolRepo != nil { 109 | _ = b.toolRepo.AddTool(ctx, tool) 110 | } 111 | return b 112 | } 113 | 114 | // AddResource adds a resource to the server's resource repository 115 | func (b *ServerBuilder) AddResource(ctx context.Context, resource *domain.Resource) *ServerBuilder { 116 | if b.resourceRepo != nil { 117 | _ = b.resourceRepo.AddResource(ctx, resource) 118 | } 119 | return b 120 | } 121 | 122 | // AddPrompt adds a prompt to the server's prompt repository 123 | func (b *ServerBuilder) AddPrompt(ctx context.Context, prompt *domain.Prompt) *ServerBuilder { 124 | if b.promptRepo != nil { 125 | _ = b.promptRepo.AddPrompt(ctx, prompt) 126 | } 127 | return b 128 | } 129 | 130 | // BuildService builds and returns the server service 131 | func (b *ServerBuilder) BuildService() *usecases.ServerService { 132 | // If we already have a server service, return it 133 | if b.serverService != nil { 134 | return b.serverService 135 | } 136 | 137 | // Create notification sender if not provided 138 | if b.notificationSender == nil { 139 | b.notificationSender = server.NewNotificationSender("2.0") 140 | } 141 | 142 | // Create the server service config 143 | config := usecases.ServerConfig{ 144 | Name: b.name, 145 | Version: b.version, 146 | Instructions: b.instructions, 147 | ResourceRepo: b.resourceRepo, 148 | ToolRepo: b.toolRepo, 149 | PromptRepo: b.promptRepo, 150 | SessionRepo: b.sessionRepo, 151 | NotificationSender: b.notificationSender, 152 | } 153 | 154 | // Create and store the server service 155 | b.serverService = usecases.NewServerService(config) 156 | return b.serverService 157 | } 158 | 159 | // BuildMCPServer builds and returns an MCP server 160 | func (b *ServerBuilder) BuildMCPServer() *rest.MCPServer { 161 | service := b.BuildService() 162 | return rest.NewMCPServer(service, b.address) 163 | } 164 | 165 | // BuildStdioServer builds a stdio server that uses the MCP server 166 | func (b *ServerBuilder) BuildStdioServer(opts ...stdio.StdioOption) *stdio.StdioServer { 167 | mcpServer := b.BuildMCPServer() 168 | return stdio.NewStdioServer(mcpServer, opts...) 169 | } 170 | 171 | // ServeStdio builds and starts serving a stdio server 172 | func (b *ServerBuilder) ServeStdio(opts ...stdio.StdioOption) error { 173 | // Create a default logger for stdio 174 | logger, err := logging.New(logging.Config{ 175 | Level: logging.InfoLevel, 176 | Development: true, 177 | OutputPaths: []string{"stderr"}, 178 | InitialFields: logging.Fields{ 179 | "component": "stdio-server", 180 | }, 181 | }) 182 | if err != nil { 183 | // If we can't create the logger, continue with the options provided 184 | // The stdio server will create its own default logger 185 | } else { 186 | // Prepend the logger option so it can be overridden by user-provided options 187 | opts = append([]stdio.StdioOption{stdio.WithLogger(logger)}, opts...) 188 | } 189 | 190 | mcpServer := b.BuildMCPServer() 191 | return stdio.ServeStdio(mcpServer, opts...) 192 | } 193 | -------------------------------------------------------------------------------- /internal/domain/errors.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "fmt" 4 | 5 | // Common domain errors 6 | var ( 7 | ErrNotFound = NewError("not found", 404) 8 | ErrUnauthorized = NewError("unauthorized", 401) 9 | ErrInvalidInput = NewError("invalid input", 400) 10 | ErrInternal = NewError("internal server error", 500) 11 | ErrNotImplemented = NewError("not implemented", 501) 12 | ) 13 | 14 | // Error represents a domain error with an associated code. 15 | type Error struct { 16 | Message string 17 | Code int 18 | } 19 | 20 | // Error returns the error message. 21 | func (e *Error) Error() string { 22 | return e.Message 23 | } 24 | 25 | // NewError creates a new domain error with the given message and code. 26 | func NewError(message string, code int) *Error { 27 | return &Error{ 28 | Message: message, 29 | Code: code, 30 | } 31 | } 32 | 33 | // ResourceNotFoundError indicates that a requested resource was not found. 34 | type ResourceNotFoundError struct { 35 | URI string 36 | Err *Error 37 | } 38 | 39 | // Error returns the error message. 40 | func (e *ResourceNotFoundError) Error() string { 41 | return e.Err.Error() 42 | } 43 | 44 | // NewResourceNotFoundError creates a new ResourceNotFoundError. 45 | func NewResourceNotFoundError(uri string) *ResourceNotFoundError { 46 | return &ResourceNotFoundError{ 47 | URI: uri, 48 | Err: NewError( 49 | fmt.Sprintf("resource with URI %s not found", uri), 50 | 404, 51 | ), 52 | } 53 | } 54 | 55 | // ToolNotFoundError indicates that a requested tool was not found. 56 | type ToolNotFoundError struct { 57 | Name string 58 | Err *Error 59 | } 60 | 61 | // Error returns the error message. 62 | func (e *ToolNotFoundError) Error() string { 63 | return e.Err.Error() 64 | } 65 | 66 | // NewToolNotFoundError creates a new ToolNotFoundError. 67 | func NewToolNotFoundError(name string) *ToolNotFoundError { 68 | return &ToolNotFoundError{ 69 | Name: name, 70 | Err: NewError( 71 | fmt.Sprintf("tool with name %s not found", name), 72 | 404, 73 | ), 74 | } 75 | } 76 | 77 | // PromptNotFoundError indicates that a requested prompt was not found. 78 | type PromptNotFoundError struct { 79 | Name string 80 | Err *Error 81 | } 82 | 83 | // Error returns the error message. 84 | func (e *PromptNotFoundError) Error() string { 85 | return e.Err.Error() 86 | } 87 | 88 | // NewPromptNotFoundError creates a new PromptNotFoundError. 89 | func NewPromptNotFoundError(name string) *PromptNotFoundError { 90 | return &PromptNotFoundError{ 91 | Name: name, 92 | Err: NewError( 93 | fmt.Sprintf("prompt with name %s not found", name), 94 | 404, 95 | ), 96 | } 97 | } 98 | 99 | // SessionNotFoundError indicates that a requested session was not found. 100 | type SessionNotFoundError struct { 101 | ID string 102 | Err *Error 103 | } 104 | 105 | // Error returns the error message. 106 | func (e *SessionNotFoundError) Error() string { 107 | return e.Err.Error() 108 | } 109 | 110 | // NewSessionNotFoundError creates a new SessionNotFoundError. 111 | func NewSessionNotFoundError(id string) *SessionNotFoundError { 112 | return &SessionNotFoundError{ 113 | ID: id, 114 | Err: NewError( 115 | fmt.Sprintf("session with ID %s not found", id), 116 | 404, 117 | ), 118 | } 119 | } 120 | 121 | // ValidationError indicates that input validation failed. 122 | type ValidationError struct { 123 | Field string 124 | Message string 125 | Err *Error 126 | } 127 | 128 | // Error returns the error message. 129 | func (e *ValidationError) Error() string { 130 | return e.Err.Error() 131 | } 132 | 133 | // NewValidationError creates a new ValidationError. 134 | func NewValidationError(field, message string) *ValidationError { 135 | return &ValidationError{ 136 | Field: field, 137 | Message: message, 138 | Err: NewError( 139 | fmt.Sprintf("validation failed for field %s: %s", field, message), 140 | 400, 141 | ), 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /internal/domain/errors_test.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestDomainError(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | message string 11 | code int 12 | }{ 13 | { 14 | name: "Not found error", 15 | message: "not found", 16 | code: 404, 17 | }, 18 | { 19 | name: "Invalid input error", 20 | message: "invalid input", 21 | code: 400, 22 | }, 23 | { 24 | name: "Internal server error", 25 | message: "internal server error", 26 | code: 500, 27 | }, 28 | } 29 | 30 | for _, tt := range tests { 31 | t.Run(tt.name, func(t *testing.T) { 32 | err := NewError(tt.message, tt.code) 33 | 34 | if err.Message != tt.message { 35 | t.Errorf("NewError().Message = %v, want %v", err.Message, tt.message) 36 | } 37 | if err.Code != tt.code { 38 | t.Errorf("NewError().Code = %v, want %v", err.Code, tt.code) 39 | } 40 | if err.Error() != tt.message { 41 | t.Errorf("NewError().Error() = %v, want %v", err.Error(), tt.message) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestResourceNotFoundError(t *testing.T) { 48 | uri := "test/resource" 49 | err := NewResourceNotFoundError(uri) 50 | 51 | if err.URI != uri { 52 | t.Errorf("NewResourceNotFoundError().URI = %v, want %v", err.URI, uri) 53 | } 54 | if err.Err == nil { 55 | t.Error("NewResourceNotFoundError().Err should not be nil") 56 | } 57 | if err.Err.Code != 404 { 58 | t.Errorf("NewResourceNotFoundError().Err.Code = %v, want 404", err.Err.Code) 59 | } 60 | if err.Error() == "" { 61 | t.Error("NewResourceNotFoundError().Error() should not return empty string") 62 | } 63 | } 64 | 65 | func TestToolNotFoundError(t *testing.T) { 66 | name := "test-tool" 67 | err := NewToolNotFoundError(name) 68 | 69 | if err.Name != name { 70 | t.Errorf("NewToolNotFoundError().Name = %v, want %v", err.Name, name) 71 | } 72 | if err.Err == nil { 73 | t.Error("NewToolNotFoundError().Err should not be nil") 74 | } 75 | if err.Err.Code != 404 { 76 | t.Errorf("NewToolNotFoundError().Err.Code = %v, want 404", err.Err.Code) 77 | } 78 | if err.Error() == "" { 79 | t.Error("NewToolNotFoundError().Error() should not return empty string") 80 | } 81 | } 82 | 83 | func TestPromptNotFoundError(t *testing.T) { 84 | name := "test-prompt" 85 | err := NewPromptNotFoundError(name) 86 | 87 | if err.Name != name { 88 | t.Errorf("NewPromptNotFoundError().Name = %v, want %v", err.Name, name) 89 | } 90 | if err.Err == nil { 91 | t.Error("NewPromptNotFoundError().Err should not be nil") 92 | } 93 | if err.Err.Code != 404 { 94 | t.Errorf("NewPromptNotFoundError().Err.Code = %v, want 404", err.Err.Code) 95 | } 96 | if err.Error() == "" { 97 | t.Error("NewPromptNotFoundError().Error() should not return empty string") 98 | } 99 | } 100 | 101 | func TestSessionNotFoundError(t *testing.T) { 102 | id := "test-session-id" 103 | err := NewSessionNotFoundError(id) 104 | 105 | if err.ID != id { 106 | t.Errorf("NewSessionNotFoundError().ID = %v, want %v", err.ID, id) 107 | } 108 | if err.Err == nil { 109 | t.Error("NewSessionNotFoundError().Err should not be nil") 110 | } 111 | if err.Err.Code != 404 { 112 | t.Errorf("NewSessionNotFoundError().Err.Code = %v, want 404", err.Err.Code) 113 | } 114 | if err.Error() == "" { 115 | t.Error("NewSessionNotFoundError().Error() should not return empty string") 116 | } 117 | } 118 | 119 | func TestValidationError(t *testing.T) { 120 | field := "name" 121 | message := "must not be empty" 122 | err := NewValidationError(field, message) 123 | 124 | if err.Field != field { 125 | t.Errorf("NewValidationError().Field = %v, want %v", err.Field, field) 126 | } 127 | if err.Message != message { 128 | t.Errorf("NewValidationError().Message = %v, want %v", err.Message, message) 129 | } 130 | if err.Err == nil { 131 | t.Error("NewValidationError().Err should not be nil") 132 | } 133 | if err.Err.Code != 400 { 134 | t.Errorf("NewValidationError().Err.Code = %v, want 400", err.Err.Code) 135 | } 136 | if err.Error() == "" { 137 | t.Error("NewValidationError().Error() should not return empty string") 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /internal/domain/jsonrpc_models.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | // JSONRPCRequest represents a JSON-RPC request in the domain layer. 4 | type JSONRPCRequest struct { 5 | JSONRPC string `json:"jsonrpc"` 6 | ID interface{} `json:"id"` 7 | Method string `json:"method"` 8 | Params interface{} `json:"params,omitempty"` 9 | } 10 | 11 | // JSONRPCResponse represents a JSON-RPC response in the domain layer. 12 | type JSONRPCResponse struct { 13 | JSONRPC string `json:"jsonrpc"` 14 | ID interface{} `json:"id"` 15 | Result interface{} `json:"result,omitempty"` 16 | Error *JSONRPCError `json:"error,omitempty"` 17 | } 18 | 19 | // JSONRPCError represents a JSON-RPC error in the domain layer. 20 | type JSONRPCError struct { 21 | Code int `json:"code"` 22 | Message string `json:"message"` 23 | Data interface{} `json:"data,omitempty"` 24 | } 25 | 26 | // CreateResponse creates a new JSONRPCResponse with the given ID and result. 27 | func CreateResponse(jsonrpcVersion string, id interface{}, result interface{}) JSONRPCResponse { 28 | return JSONRPCResponse{ 29 | JSONRPC: jsonrpcVersion, 30 | ID: id, 31 | Result: result, 32 | } 33 | } 34 | 35 | // CreateErrorResponse creates a new JSONRPCResponse with the given ID and error. 36 | func CreateErrorResponse(jsonrpcVersion string, id interface{}, code int, message string) JSONRPCResponse { 37 | return JSONRPCResponse{ 38 | JSONRPC: jsonrpcVersion, 39 | ID: id, 40 | Error: &JSONRPCError{ 41 | Code: code, 42 | Message: message, 43 | }, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /internal/domain/jsonrpc_models_test.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCreateResponse(t *testing.T) { 8 | // Test parameters 9 | jsonrpcVersion := "2.0" 10 | id := 123 11 | result := map[string]interface{}{ 12 | "success": true, 13 | "data": "test data", 14 | } 15 | 16 | // Create response 17 | response := CreateResponse(jsonrpcVersion, id, result) 18 | 19 | // Assertions 20 | if response.JSONRPC != jsonrpcVersion { 21 | t.Errorf("CreateResponse().JSONRPC = %v, want %v", response.JSONRPC, jsonrpcVersion) 22 | } 23 | if response.ID != id { 24 | t.Errorf("CreateResponse().ID = %v, want %v", response.ID, id) 25 | } 26 | if response.Result == nil { 27 | t.Error("CreateResponse().Result should not be nil") 28 | } 29 | if response.Error != nil { 30 | t.Error("CreateResponse().Error should be nil") 31 | } 32 | 33 | // Type assertion and validation of result 34 | resultMap, ok := response.Result.(map[string]interface{}) 35 | if !ok { 36 | t.Errorf("CreateResponse().Result should be a map[string]interface{}, got %T", response.Result) 37 | } else { 38 | if resultMap["success"] != result["success"] { 39 | t.Errorf("CreateResponse().Result[\"success\"] = %v, want %v", resultMap["success"], result["success"]) 40 | } 41 | if resultMap["data"] != result["data"] { 42 | t.Errorf("CreateResponse().Result[\"data\"] = %v, want %v", resultMap["data"], result["data"]) 43 | } 44 | } 45 | } 46 | 47 | func TestCreateErrorResponse(t *testing.T) { 48 | // Test parameters 49 | jsonrpcVersion := "2.0" 50 | id := "abc123" 51 | code := -32600 52 | message := "Invalid Request" 53 | 54 | // Create error response 55 | response := CreateErrorResponse(jsonrpcVersion, id, code, message) 56 | 57 | // Assertions 58 | if response.JSONRPC != jsonrpcVersion { 59 | t.Errorf("CreateErrorResponse().JSONRPC = %v, want %v", response.JSONRPC, jsonrpcVersion) 60 | } 61 | if response.ID != id { 62 | t.Errorf("CreateErrorResponse().ID = %v, want %v", response.ID, id) 63 | } 64 | if response.Result != nil { 65 | t.Error("CreateErrorResponse().Result should be nil") 66 | } 67 | if response.Error == nil { 68 | t.Error("CreateErrorResponse().Error should not be nil") 69 | } else { 70 | if response.Error.Code != code { 71 | t.Errorf("CreateErrorResponse().Error.Code = %v, want %v", response.Error.Code, code) 72 | } 73 | if response.Error.Message != message { 74 | t.Errorf("CreateErrorResponse().Error.Message = %v, want %v", response.Error.Message, message) 75 | } 76 | } 77 | } 78 | 79 | func TestJSONRPCRequest(t *testing.T) { 80 | // Create request 81 | request := JSONRPCRequest{ 82 | JSONRPC: "2.0", 83 | ID: 456, 84 | Method: "test.method", 85 | Params: map[string]interface{}{ 86 | "param1": "value1", 87 | "param2": 42, 88 | }, 89 | } 90 | 91 | // Assertions 92 | if request.JSONRPC != "2.0" { 93 | t.Errorf("JSONRPCRequest.JSONRPC = %v, want %v", request.JSONRPC, "2.0") 94 | } 95 | if request.ID != 456 { 96 | t.Errorf("JSONRPCRequest.ID = %v, want %v", request.ID, 456) 97 | } 98 | if request.Method != "test.method" { 99 | t.Errorf("JSONRPCRequest.Method = %v, want %v", request.Method, "test.method") 100 | } 101 | 102 | // Type assertion and validation of params 103 | paramsMap, ok := request.Params.(map[string]interface{}) 104 | if !ok { 105 | t.Errorf("JSONRPCRequest.Params should be a map[string]interface{}, got %T", request.Params) 106 | } else { 107 | if paramsMap["param1"] != "value1" { 108 | t.Errorf("JSONRPCRequest.Params[\"param1\"] = %v, want %v", paramsMap["param1"], "value1") 109 | } 110 | if paramsMap["param2"] != 42 { 111 | t.Errorf("JSONRPCRequest.Params[\"param2\"] = %v, want %v", paramsMap["param2"], 42) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /internal/domain/notification_models.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | // JSONRPCNotification represents a notification sent to clients via JSON-RPC. 4 | type JSONRPCNotification struct { 5 | JSONRPC string `json:"jsonrpc"` 6 | Method string `json:"method"` 7 | Params map[string]interface{} `json:"params,omitempty"` 8 | } 9 | 10 | // ToJSONRPC converts a domain Notification to a JSONRPCNotification. 11 | func (n *Notification) ToJSONRPC(jsonrpcVersion string) JSONRPCNotification { 12 | return JSONRPCNotification{ 13 | JSONRPC: jsonrpcVersion, 14 | Method: n.Method, 15 | Params: n.Params, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/domain/notification_models_test.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestNotificationToJSONRPC(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | notification *Notification 12 | jsonrpcVersion string 13 | want JSONRPCNotification 14 | }{ 15 | { 16 | name: "Basic notification", 17 | notification: &Notification{ 18 | Method: "test.notification", 19 | Params: map[string]interface{}{ 20 | "key": "value", 21 | }, 22 | }, 23 | jsonrpcVersion: "2.0", 24 | want: JSONRPCNotification{ 25 | JSONRPC: "2.0", 26 | Method: "test.notification", 27 | Params: map[string]interface{}{ 28 | "key": "value", 29 | }, 30 | }, 31 | }, 32 | { 33 | name: "Empty params", 34 | notification: &Notification{ 35 | Method: "test.empty", 36 | Params: map[string]interface{}{}, 37 | }, 38 | jsonrpcVersion: "2.0", 39 | want: JSONRPCNotification{ 40 | JSONRPC: "2.0", 41 | Method: "test.empty", 42 | Params: map[string]interface{}{}, 43 | }, 44 | }, 45 | { 46 | name: "Complex params", 47 | notification: &Notification{ 48 | Method: "test.complex", 49 | Params: map[string]interface{}{ 50 | "string": "text", 51 | "number": 123, 52 | "boolean": true, 53 | "array": []interface{}{1, 2, 3}, 54 | "object": map[string]interface{}{ 55 | "nested": "value", 56 | }, 57 | }, 58 | }, 59 | jsonrpcVersion: "2.0", 60 | want: JSONRPCNotification{ 61 | JSONRPC: "2.0", 62 | Method: "test.complex", 63 | Params: map[string]interface{}{ 64 | "string": "text", 65 | "number": 123, 66 | "boolean": true, 67 | "array": []interface{}{1, 2, 3}, 68 | "object": map[string]interface{}{ 69 | "nested": "value", 70 | }, 71 | }, 72 | }, 73 | }, 74 | } 75 | 76 | for _, tt := range tests { 77 | t.Run(tt.name, func(t *testing.T) { 78 | got := tt.notification.ToJSONRPC(tt.jsonrpcVersion) 79 | 80 | // Check JSONRPC version 81 | if got.JSONRPC != tt.want.JSONRPC { 82 | t.Errorf("ToJSONRPC().JSONRPC = %v, want %v", got.JSONRPC, tt.want.JSONRPC) 83 | } 84 | 85 | // Check Method 86 | if got.Method != tt.want.Method { 87 | t.Errorf("ToJSONRPC().Method = %v, want %v", got.Method, tt.want.Method) 88 | } 89 | 90 | // Compare Params using JSON marshaling for deep equality 91 | gotJSON, err := json.Marshal(got.Params) 92 | if err != nil { 93 | t.Errorf("Failed to marshal got.Params: %v", err) 94 | } 95 | 96 | wantJSON, err := json.Marshal(tt.want.Params) 97 | if err != nil { 98 | t.Errorf("Failed to marshal want.Params: %v", err) 99 | } 100 | 101 | if string(gotJSON) != string(wantJSON) { 102 | t.Errorf("ToJSONRPC().Params = %v, want %v", string(gotJSON), string(wantJSON)) 103 | } 104 | }) 105 | } 106 | } 107 | 108 | func TestJSONRPCNotificationMarshal(t *testing.T) { 109 | notification := JSONRPCNotification{ 110 | JSONRPC: "2.0", 111 | Method: "test.notification", 112 | Params: map[string]interface{}{ 113 | "key": "value", 114 | "number": 42, 115 | }, 116 | } 117 | 118 | // Marshal to JSON 119 | jsonBytes, err := json.Marshal(notification) 120 | if err != nil { 121 | t.Fatalf("Failed to marshal JSONRPCNotification: %v", err) 122 | } 123 | 124 | // Unmarshal back to verify JSON structure 125 | var unmarshaled map[string]interface{} 126 | err = json.Unmarshal(jsonBytes, &unmarshaled) 127 | if err != nil { 128 | t.Fatalf("Failed to unmarshal JSON: %v", err) 129 | } 130 | 131 | // Check fields 132 | if unmarshaled["jsonrpc"] != "2.0" { 133 | t.Errorf("jsonrpc = %v, want %v", unmarshaled["jsonrpc"], "2.0") 134 | } 135 | 136 | if unmarshaled["method"] != "test.notification" { 137 | t.Errorf("method = %v, want %v", unmarshaled["method"], "test.notification") 138 | } 139 | 140 | params, ok := unmarshaled["params"].(map[string]interface{}) 141 | if !ok { 142 | t.Fatalf("params is not a map[string]interface{}, got %T", unmarshaled["params"]) 143 | } 144 | 145 | if params["key"] != "value" { 146 | t.Errorf("params.key = %v, want %v", params["key"], "value") 147 | } 148 | 149 | // JSON numbers are float64 when unmarshaled 150 | if params["number"] != float64(42) { 151 | t.Errorf("params.number = %v, want %v", params["number"], float64(42)) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /internal/domain/repositories.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "context" 4 | 5 | // ResourceRepository defines the interface for managing resources. 6 | type ResourceRepository interface { 7 | // GetResource retrieves a resource by its URI. 8 | GetResource(ctx context.Context, uri string) (*Resource, error) 9 | 10 | // ListResources returns all available resources. 11 | ListResources(ctx context.Context) ([]*Resource, error) 12 | 13 | // AddResource adds a new resource to the repository. 14 | AddResource(ctx context.Context, resource *Resource) error 15 | 16 | // DeleteResource removes a resource from the repository. 17 | DeleteResource(ctx context.Context, uri string) error 18 | } 19 | 20 | // ToolRepository defines the interface for managing tools. 21 | type ToolRepository interface { 22 | // GetTool retrieves a tool by its name. 23 | GetTool(ctx context.Context, name string) (*Tool, error) 24 | 25 | // ListTools returns all available tools. 26 | ListTools(ctx context.Context) ([]*Tool, error) 27 | 28 | // AddTool adds a new tool to the repository. 29 | AddTool(ctx context.Context, tool *Tool) error 30 | 31 | // DeleteTool removes a tool from the repository. 32 | DeleteTool(ctx context.Context, name string) error 33 | } 34 | 35 | // PromptRepository defines the interface for managing prompts. 36 | type PromptRepository interface { 37 | // GetPrompt retrieves a prompt by its name. 38 | GetPrompt(ctx context.Context, name string) (*Prompt, error) 39 | 40 | // ListPrompts returns all available prompts. 41 | ListPrompts(ctx context.Context) ([]*Prompt, error) 42 | 43 | // AddPrompt adds a new prompt to the repository. 44 | AddPrompt(ctx context.Context, prompt *Prompt) error 45 | 46 | // DeletePrompt removes a prompt from the repository. 47 | DeletePrompt(ctx context.Context, name string) error 48 | } 49 | 50 | // SessionRepository defines the interface for managing client sessions. 51 | type SessionRepository interface { 52 | // GetSession retrieves a session by its ID. 53 | GetSession(ctx context.Context, id string) (*ClientSession, error) 54 | 55 | // ListSessions returns all active sessions. 56 | ListSessions(ctx context.Context) ([]*ClientSession, error) 57 | 58 | // AddSession adds a new session to the repository. 59 | AddSession(ctx context.Context, session *ClientSession) error 60 | 61 | // DeleteSession removes a session from the repository. 62 | DeleteSession(ctx context.Context, id string) error 63 | } 64 | 65 | // NotificationSender defines the interface for sending notifications to clients. 66 | type NotificationSender interface { 67 | // SendNotification sends a notification to a specific client. 68 | SendNotification(ctx context.Context, sessionID string, notification *Notification) error 69 | 70 | // BroadcastNotification sends a notification to all connected clients. 71 | BroadcastNotification(ctx context.Context, notification *Notification) error 72 | } 73 | -------------------------------------------------------------------------------- /internal/domain/sse_interfaces.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | ) 8 | 9 | // SSEHandler defines the interface for a server-sent events handler. 10 | type SSEHandler interface { 11 | // ServeHTTP handles HTTP requests for SSE events. 12 | ServeHTTP(w http.ResponseWriter, r *http.Request) 13 | 14 | // Start starts the SSE server. 15 | Start(addr string) error 16 | 17 | // Shutdown gracefully stops the SSE server. 18 | Shutdown(ctx context.Context) error 19 | 20 | // BroadcastEvent sends an event to all connected clients. 21 | BroadcastEvent(event interface{}) error 22 | 23 | // SendEventToSession sends an event to a specific client session. 24 | SendEventToSession(sessionID string, event interface{}) error 25 | } 26 | 27 | // MessageHandler defines the interface for handling message processing in the SSE server. 28 | type MessageHandler interface { 29 | // HandleMessage processes a raw JSON message and returns a response. 30 | HandleMessage(ctx context.Context, rawMessage json.RawMessage) interface{} 31 | } 32 | 33 | // SSESession represents an active SSE connection. 34 | type SSESession interface { 35 | // ID returns the session identifier. 36 | ID() string 37 | 38 | // Close closes the session. 39 | Close() 40 | 41 | // NotificationChannel returns the channel used to send notifications to this session. 42 | NotificationChannel() chan<- string 43 | 44 | // Start begins processing events for this session. 45 | Start() 46 | 47 | // Context returns the session's context. 48 | Context() context.Context 49 | } 50 | 51 | // ConnectionManager defines the interface for managing SSE connections. 52 | type ConnectionManager interface { 53 | // Add adds a session to the manager. 54 | AddSession(session SSESession) 55 | 56 | // Remove removes a session from the manager. 57 | RemoveSession(sessionID string) 58 | 59 | // Get retrieves a session by ID. 60 | GetSession(sessionID string) (SSESession, bool) 61 | 62 | // Broadcast sends an event to all sessions. 63 | Broadcast(event interface{}) error 64 | 65 | // CloseAll closes all active sessions. 66 | CloseAll() 67 | 68 | // Count returns the number of active sessions. 69 | Count() int 70 | } 71 | -------------------------------------------------------------------------------- /internal/domain/types.go: -------------------------------------------------------------------------------- 1 | // Package domain defines the core business logic and entities for the MCP server. 2 | package domain 3 | 4 | import ( 5 | "github.com/google/uuid" 6 | ) 7 | 8 | // ClientSession represents an active client connection to the MCP server. 9 | type ClientSession struct { 10 | ID string 11 | UserAgent string 12 | Connected bool 13 | } 14 | 15 | // NewClientSession creates a new ClientSession with a unique ID. 16 | func NewClientSession(userAgent string) *ClientSession { 17 | return &ClientSession{ 18 | ID: uuid.New().String(), 19 | UserAgent: userAgent, 20 | Connected: true, 21 | } 22 | } 23 | 24 | // Resource represents a resource that can be requested by clients. 25 | type Resource struct { 26 | URI string 27 | Name string 28 | Description string 29 | MIMEType string 30 | } 31 | 32 | // ResourceContents represents the contents of a resource. 33 | type ResourceContents struct { 34 | URI string 35 | MIMEType string 36 | Content []byte 37 | Text string 38 | } 39 | 40 | // Tool represents a tool that can be called by clients. 41 | type Tool struct { 42 | Name string 43 | Description string 44 | Parameters []ToolParameter 45 | } 46 | 47 | // ToolParameter defines a parameter for a tool. 48 | type ToolParameter struct { 49 | Name string 50 | Description string 51 | Type string 52 | Required bool 53 | Items map[string]interface{} 54 | } 55 | 56 | // ToolCall represents a request to execute a tool. 57 | type ToolCall struct { 58 | Name string 59 | Parameters map[string]interface{} 60 | Session *ClientSession 61 | } 62 | 63 | // ToolResult represents the result of a tool execution. 64 | type ToolResult struct { 65 | Data interface{} 66 | Error error 67 | } 68 | 69 | // Prompt represents a prompt template that can be rendered. 70 | type Prompt struct { 71 | Name string 72 | Description string 73 | Template string 74 | Parameters []PromptParameter 75 | } 76 | 77 | // PromptParameter defines a parameter for a prompt template. 78 | type PromptParameter struct { 79 | Name string 80 | Description string 81 | Type string 82 | Required bool 83 | } 84 | 85 | // PromptRequest represents a request to render a prompt. 86 | type PromptRequest struct { 87 | Name string 88 | Parameters map[string]interface{} 89 | Session *ClientSession 90 | } 91 | 92 | // PromptResult represents the result of a prompt rendering. 93 | type PromptResult struct { 94 | Text string 95 | Error error 96 | } 97 | 98 | // Notification represents a notification that can be sent to clients. 99 | type Notification struct { 100 | Method string 101 | Params map[string]interface{} 102 | } 103 | -------------------------------------------------------------------------------- /internal/domain/types_test.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | func TestNewClientSession(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | userAgent string 13 | want *ClientSession 14 | }{ 15 | { 16 | name: "Valid user agent", 17 | userAgent: "test-agent", 18 | want: &ClientSession{ 19 | UserAgent: "test-agent", 20 | Connected: true, 21 | }, 22 | }, 23 | { 24 | name: "Empty user agent", 25 | userAgent: "", 26 | want: &ClientSession{ 27 | UserAgent: "", 28 | Connected: true, 29 | }, 30 | }, 31 | } 32 | 33 | for _, tt := range tests { 34 | t.Run(tt.name, func(t *testing.T) { 35 | got := NewClientSession(tt.userAgent) 36 | 37 | // Verify user agent and connected status 38 | if got.UserAgent != tt.want.UserAgent { 39 | t.Errorf("NewClientSession().UserAgent = %v, want %v", got.UserAgent, tt.want.UserAgent) 40 | } 41 | if got.Connected != tt.want.Connected { 42 | t.Errorf("NewClientSession().Connected = %v, want %v", got.Connected, tt.want.Connected) 43 | } 44 | 45 | // Check if ID is a valid UUID 46 | _, err := uuid.Parse(got.ID) 47 | if err != nil { 48 | t.Errorf("NewClientSession().ID is not a valid UUID: %v", err) 49 | } 50 | }) 51 | } 52 | } 53 | 54 | func TestResourceContents(t *testing.T) { 55 | tests := []struct { 56 | name string 57 | contents ResourceContents 58 | }{ 59 | { 60 | name: "Text content", 61 | contents: ResourceContents{ 62 | URI: "test/uri.txt", 63 | MIMEType: "text/plain", 64 | Content: []byte("Hello, World!"), 65 | Text: "Hello, World!", 66 | }, 67 | }, 68 | { 69 | name: "Binary content", 70 | contents: ResourceContents{ 71 | URI: "test/image.png", 72 | MIMEType: "image/png", 73 | Content: []byte{0x89, 0x50, 0x4E, 0x47}, 74 | Text: "", 75 | }, 76 | }, 77 | } 78 | 79 | for _, tt := range tests { 80 | t.Run(tt.name, func(t *testing.T) { 81 | // Verify fields are correctly set 82 | if tt.contents.URI == "" { 83 | t.Error("ResourceContents.URI should not be empty") 84 | } 85 | if tt.contents.MIMEType == "" { 86 | t.Error("ResourceContents.MIMEType should not be empty") 87 | } 88 | if len(tt.contents.Content) == 0 { 89 | t.Error("ResourceContents.Content should not be empty") 90 | } 91 | }) 92 | } 93 | } 94 | 95 | func TestNotification(t *testing.T) { 96 | tests := []struct { 97 | name string 98 | notification Notification 99 | }{ 100 | { 101 | name: "Empty params", 102 | notification: Notification{ 103 | Method: "test/method", 104 | Params: map[string]interface{}{}, 105 | }, 106 | }, 107 | { 108 | name: "With params", 109 | notification: Notification{ 110 | Method: "test/method", 111 | Params: map[string]interface{}{ 112 | "key": "value", 113 | "numeric": 42, 114 | "boolean": true, 115 | }, 116 | }, 117 | }, 118 | } 119 | 120 | for _, tt := range tests { 121 | t.Run(tt.name, func(t *testing.T) { 122 | // Verify fields are correctly set 123 | if tt.notification.Method == "" { 124 | t.Error("Notification.Method should not be empty") 125 | } 126 | if tt.notification.Params == nil { 127 | t.Error("Notification.Params should not be nil") 128 | } 129 | }) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /internal/infrastructure/logging/README.md: -------------------------------------------------------------------------------- 1 | # Logging Package 2 | 3 | This package provides a structured logging implementation for the MCP Server SDK, built on top of [Uber's zap](https://github.com/uber-go/zap) library. 4 | 5 | ## Features 6 | 7 | - Multiple log levels (Debug, Info, Warn, Error, Fatal, Panic) 8 | - Structured logging with fields 9 | - Context-aware logging 10 | - Printf-style logging 11 | - High performance JSON logging 12 | - Configurable output paths 13 | 14 | ## Usage 15 | 16 | ### Basic Logging 17 | 18 | ```go 19 | package main 20 | 21 | import ( 22 | "github.com/FreePeak/cortex/internal/infrastructure/logging" 23 | ) 24 | 25 | func main() { 26 | // Create a development logger (with debug level enabled) 27 | logger, err := logging.NewDevelopment() 28 | if err != nil { 29 | panic(err) 30 | } 31 | defer logger.Sync() // Flushes buffer, if any 32 | 33 | // Simple logging 34 | logger.Debug("Debug message") 35 | logger.Info("Info message") 36 | logger.Warn("Warning message") 37 | logger.Error("Error message") 38 | // logger.Fatal("Fatal message") // This would exit the program 39 | // logger.Panic("Panic message") // This would panic 40 | } 41 | ``` 42 | 43 | ### Structured Logging with Fields 44 | 45 | ```go 46 | logger.Info("User logged in", logging.Fields{ 47 | "user_id": 123, 48 | "email": "user@example.com", 49 | }) 50 | 51 | // Create a logger with default fields 52 | userLogger := logger.With(logging.Fields{ 53 | "user_id": 123, 54 | "email": "user@example.com", 55 | }) 56 | 57 | userLogger.Info("User profile updated") 58 | userLogger.Error("Failed to update password") 59 | ``` 60 | 61 | ### Context-Aware Logging 62 | 63 | ```go 64 | ctx := context.Background() 65 | logger.InfoContext(ctx, "Starting operation") 66 | logger.ErrorContext(ctx, "Operation failed", logging.Fields{ 67 | "error": "connection timeout", 68 | }) 69 | ``` 70 | 71 | ### Formatted Logging 72 | 73 | ```go 74 | logger.Infof("User %d logged in from %s", 123, "192.168.1.1") 75 | logger.Errorf("Failed to process payment: %v", fmt.Errorf("insufficient funds")) 76 | ``` 77 | 78 | ### Custom Configuration 79 | 80 | ```go 81 | // Create a custom logger configuration 82 | config := logging.Config{ 83 | Level: logging.DebugLevel, 84 | Development: true, 85 | OutputPaths: []string{"stdout", "logs/app.log"}, 86 | InitialFields: logging.Fields{ 87 | "app": "example-app", 88 | "env": "testing", 89 | }, 90 | } 91 | 92 | // Create a logger with custom config 93 | logger, err := logging.New(config) 94 | if err != nil { 95 | panic(err) 96 | } 97 | defer logger.Sync() 98 | 99 | logger.Info("Application started") 100 | ``` 101 | 102 | ### Default Logger 103 | 104 | The package provides a default logger that can be used throughout your application: 105 | 106 | ```go 107 | // Get the default logger (production level) 108 | logger := logging.Default() 109 | logger.Info("Using default logger") 110 | 111 | // You can also replace the default logger 112 | customLogger, _ := logging.NewDevelopment() 113 | logging.SetDefault(customLogger) 114 | ``` 115 | 116 | ## Available Log Levels 117 | 118 | - `DebugLevel`: Debug information, most verbose 119 | - `InfoLevel`: General operational information 120 | - `WarnLevel`: Warning conditions, not critical but should be checked 121 | - `ErrorLevel`: Error conditions, likely requiring attention 122 | - `FatalLevel`: Fatal conditions, will call `os.Exit(1)` 123 | - `PanicLevel`: Panic conditions, will call `panic()` -------------------------------------------------------------------------------- /internal/infrastructure/logging/example_test.go: -------------------------------------------------------------------------------- 1 | package logging_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/FreePeak/cortex/internal/infrastructure/logging" 8 | ) 9 | 10 | func Example() { 11 | // Create a development logger (with debug level enabled) 12 | logger, err := logging.NewDevelopment() 13 | if err != nil { 14 | panic(err) 15 | } 16 | defer func() { 17 | _ = logger.Sync() // Intentionally ignoring sync errors in examples 18 | }() 19 | 20 | // Simple logging 21 | logger.Debug("Debug message") 22 | logger.Info("Info message") 23 | logger.Warn("Warning message") 24 | logger.Error("Error message") 25 | // logger.Fatal("Fatal message") // This would exit the program 26 | // logger.Panic("Panic message") // This would panic 27 | 28 | // Logging with fields 29 | logger.Info("User logged in", logging.Fields{ 30 | "user_id": 123, 31 | "email": "user@example.com", 32 | }) 33 | 34 | // Using With to create a logger with default fields 35 | userLogger := logger.With(logging.Fields{ 36 | "user_id": 123, 37 | "email": "user@example.com", 38 | }) 39 | 40 | userLogger.Info("User profile updated") 41 | userLogger.Error("Failed to update password") 42 | 43 | // Formatted logging (using sugar) 44 | logger.Infof("User %d logged in from %s", 123, "192.168.1.1") 45 | logger.Errorf("Failed to process payment: %v", fmt.Errorf("insufficient funds")) 46 | 47 | // Context-aware logging 48 | ctx := context.Background() 49 | logger.InfoContext(ctx, "Starting operation") 50 | logger.ErrorContext(ctx, "Operation failed", logging.Fields{ 51 | "error": "connection timeout", 52 | }) 53 | 54 | // Using the default logger (production level) 55 | defaultLogger := logging.Default() 56 | defaultLogger.Info("Using default logger") 57 | } 58 | 59 | func Example_customConfig() { 60 | // Create a custom logger configuration 61 | config := logging.Config{ 62 | Level: logging.DebugLevel, 63 | Development: true, 64 | OutputPaths: []string{"stdout", "logs/app.log"}, 65 | InitialFields: logging.Fields{ 66 | "app": "example-app", 67 | "env": "testing", 68 | }, 69 | } 70 | 71 | // Create a logger with custom config 72 | logger, err := logging.New(config) 73 | if err != nil { 74 | panic(err) 75 | } 76 | defer func() { 77 | _ = logger.Sync() // Intentionally ignoring sync errors in examples 78 | }() 79 | 80 | logger.Info("Application started") 81 | } 82 | 83 | func Example_productionLogger() { 84 | // Create a production logger 85 | logger, err := logging.NewProduction() 86 | if err != nil { 87 | panic(err) 88 | } 89 | defer func() { 90 | _ = logger.Sync() // Intentionally ignoring sync errors in examples 91 | }() 92 | 93 | // Production loggers typically use JSON format 94 | // and have DEBUG level disabled 95 | logger.Debug("This won't be logged in production") // Not shown 96 | logger.Info("System is running") 97 | logger.Error("Failed to connect to database", logging.Fields{ 98 | "error": "connection refused", 99 | "database": "postgres", 100 | "reconnect": true, 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /internal/infrastructure/logging/integration_example.go: -------------------------------------------------------------------------------- 1 | // This file provides examples for integrating the logging package 2 | // with the MCP server application. 3 | package logging 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | 9 | "github.com/FreePeak/cortex/internal/domain" 10 | ) 11 | 12 | // contextKey is a custom type for context keys to avoid collisions 13 | type contextKey string 14 | 15 | // Defined context keys 16 | const ( 17 | loggerKey contextKey = "logger" 18 | ) 19 | 20 | // Middleware creates an HTTP middleware that adds a logger to the request context. 21 | func Middleware(logger *Logger) func(http.Handler) http.Handler { 22 | return func(next http.Handler) http.Handler { 23 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 24 | // Create a child logger with request information 25 | requestLogger := logger.With(Fields{ 26 | "method": r.Method, 27 | "path": r.URL.Path, 28 | "remote": r.RemoteAddr, 29 | }) 30 | 31 | // Add logger to context 32 | ctx := context.WithValue(r.Context(), loggerKey, requestLogger) 33 | 34 | // Call next handler with updated context 35 | next.ServeHTTP(w, r.WithContext(ctx)) 36 | }) 37 | } 38 | } 39 | 40 | // GetLogger retrieves the logger from the context. 41 | // If no logger is found, returns a default logger. 42 | func GetLogger(ctx context.Context) *Logger { 43 | logger, ok := ctx.Value(loggerKey).(*Logger) 44 | if !ok || logger == nil { 45 | // Return default logger if not found in context 46 | return Default() 47 | } 48 | return logger 49 | } 50 | 51 | // LogJSONRPCRequest logs JSON-RPC request details 52 | func LogJSONRPCRequest(ctx context.Context, request domain.JSONRPCRequest) { 53 | logger := GetLogger(ctx) 54 | 55 | logger.Info("JSON-RPC Request", 56 | Fields{ 57 | "id": request.ID, 58 | "method": request.Method, 59 | "jsonrpc": request.JSONRPC, 60 | }) 61 | } 62 | 63 | // LogJSONRPCResponse logs JSON-RPC response details 64 | func LogJSONRPCResponse(ctx context.Context, response domain.JSONRPCResponse) { 65 | logger := GetLogger(ctx) 66 | 67 | fields := Fields{ 68 | "id": response.ID, 69 | "jsonrpc": response.JSONRPC, 70 | } 71 | 72 | if response.Error != nil { 73 | fields["error_code"] = response.Error.Code 74 | fields["error_message"] = response.Error.Message 75 | logger.Error("JSON-RPC Response Error", fields) 76 | } else { 77 | logger.Info("JSON-RPC Response Success", fields) 78 | } 79 | } 80 | 81 | // ServerStartupLogger logs server startup information 82 | func ServerStartupLogger(logger *Logger, serverName, version, address string) { 83 | logger.Info("Server starting", 84 | Fields{ 85 | "name": serverName, 86 | "version": version, 87 | "address": address, 88 | }) 89 | } 90 | 91 | // WithRequestID returns a new logger with the request ID field 92 | func WithRequestID(logger *Logger, requestID string) *Logger { 93 | return logger.With(Fields{ 94 | "request_id": requestID, 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /internal/infrastructure/server/errors.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "errors" 4 | 5 | // Common errors in the server package 6 | var ( 7 | // ErrResponseWriterNotFlusher is returned when the ResponseWriter doesn't support Flusher interface 8 | ErrResponseWriterNotFlusher = errors.New("response writer does not implement http.Flusher") 9 | 10 | // ErrSessionNotFound is returned when a session cannot be found 11 | ErrSessionNotFound = errors.New("session not found") 12 | 13 | // ErrSessionClosed is returned when attempting to use a closed session 14 | ErrSessionClosed = errors.New("session is closed") 15 | 16 | // ErrChannelFull is returned when a notification channel is full 17 | ErrChannelFull = errors.New("notification channel is full") 18 | ) 19 | -------------------------------------------------------------------------------- /internal/infrastructure/server/notification.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/FreePeak/cortex/internal/domain" 9 | ) 10 | 11 | // JSONRPCNotification represents a notification sent to clients via JSON-RPC. 12 | type JSONRPCNotification struct { 13 | JSONRPC string `json:"jsonrpc"` 14 | Method string `json:"method"` 15 | Params map[string]interface{} `json:"params,omitempty"` 16 | } 17 | 18 | // NotificationChannel is a channel for sending notifications. 19 | type NotificationChannel chan JSONRPCNotification 20 | 21 | // MCPSession represents a connected client session. 22 | type MCPSession struct { 23 | id string 24 | userAgent string 25 | notifChan NotificationChannel 26 | } 27 | 28 | // NewMCPSession creates a new MCPSession. 29 | func NewMCPSession(id, userAgent string, bufferSize int) *MCPSession { 30 | return &MCPSession{ 31 | id: id, 32 | userAgent: userAgent, 33 | notifChan: make(NotificationChannel, bufferSize), 34 | } 35 | } 36 | 37 | // ID returns the session ID. 38 | func (s *MCPSession) ID() string { 39 | return s.id 40 | } 41 | 42 | // NotificationChannel returns the channel for sending notifications to this session. 43 | func (s *MCPSession) NotificationChannel() NotificationChannel { 44 | return s.notifChan 45 | } 46 | 47 | // Close closes the notification channel. 48 | func (s *MCPSession) Close() { 49 | close(s.notifChan) 50 | } 51 | 52 | // NotificationSender handles sending notifications to clients. 53 | type NotificationSender struct { 54 | sessions sync.Map 55 | jsonrpcVersion string 56 | } 57 | 58 | // NewNotificationSender creates a new NotificationSender. 59 | func NewNotificationSender(jsonrpcVersion string) *NotificationSender { 60 | return &NotificationSender{ 61 | jsonrpcVersion: jsonrpcVersion, 62 | } 63 | } 64 | 65 | // NotificationRegistrar is an interface for registering and unregistering sessions. 66 | type NotificationRegistrar interface { 67 | // RegisterSession registers a session for notifications. 68 | RegisterSession(session *MCPSession) 69 | 70 | // UnregisterSession unregisters a session. 71 | UnregisterSession(sessionID string) 72 | } 73 | 74 | // RegisterSession registers a session for notifications. 75 | func (n *NotificationSender) RegisterSession(session *MCPSession) { 76 | n.sessions.Store(session.ID(), session) 77 | } 78 | 79 | // UnregisterSession unregisters a session. 80 | func (n *NotificationSender) UnregisterSession(sessionID string) { 81 | if session, ok := n.sessions.LoadAndDelete(sessionID); ok { 82 | if s, ok := session.(*MCPSession); ok { 83 | s.Close() 84 | } 85 | } 86 | } 87 | 88 | // SendNotification sends a notification to a specific client. 89 | func (n *NotificationSender) SendNotification(ctx context.Context, sessionID string, notification *domain.Notification) error { 90 | value, ok := n.sessions.Load(sessionID) 91 | if !ok { 92 | return fmt.Errorf("session %s not found", sessionID) 93 | } 94 | 95 | session, ok := value.(*MCPSession) 96 | if !ok { 97 | return domain.ErrInternal 98 | } 99 | 100 | jsonRPC := JSONRPCNotification{ 101 | JSONRPC: n.jsonrpcVersion, 102 | Method: notification.Method, 103 | Params: notification.Params, 104 | } 105 | 106 | select { 107 | case session.NotificationChannel() <- jsonRPC: 108 | return nil 109 | case <-ctx.Done(): 110 | return ctx.Err() 111 | default: 112 | return fmt.Errorf("notification channel for session %s is full or closed", sessionID) 113 | } 114 | } 115 | 116 | // BroadcastNotification sends a notification to all connected clients. 117 | func (n *NotificationSender) BroadcastNotification(ctx context.Context, notification *domain.Notification) error { 118 | jsonRPC := JSONRPCNotification{ 119 | JSONRPC: n.jsonrpcVersion, 120 | Method: notification.Method, 121 | Params: notification.Params, 122 | } 123 | 124 | var wg sync.WaitGroup 125 | var errsMu sync.Mutex 126 | var errs []error 127 | 128 | // First check if the context is already canceled before starting 129 | if ctx.Err() != nil { 130 | return ctx.Err() 131 | } 132 | 133 | // Function to process a session 134 | processSession := func(key, value interface{}) bool { 135 | wg.Add(1) 136 | go func() { 137 | defer wg.Done() 138 | session, ok := value.(*MCPSession) 139 | if !ok { 140 | errsMu.Lock() 141 | errs = append(errs, fmt.Errorf("invalid session type")) 142 | errsMu.Unlock() 143 | return 144 | } 145 | 146 | // Try to send notification with timeout from context 147 | select { 148 | case session.NotificationChannel() <- jsonRPC: 149 | // Successfully sent 150 | case <-ctx.Done(): 151 | errsMu.Lock() 152 | errs = append(errs, fmt.Errorf("context canceled for session %s: %w", session.ID(), ctx.Err())) 153 | errsMu.Unlock() 154 | default: 155 | errsMu.Lock() 156 | errs = append(errs, fmt.Errorf("notification channel for session %s is full or closed", session.ID())) 157 | errsMu.Unlock() 158 | } 159 | }() 160 | return true 161 | } 162 | 163 | // Process all sessions 164 | n.sessions.Range(processSession) 165 | 166 | // Wait for all goroutines to complete 167 | wg.Wait() 168 | 169 | // Return first error if any occurred 170 | if len(errs) > 0 { 171 | return errs[0] 172 | } 173 | 174 | return nil 175 | } 176 | -------------------------------------------------------------------------------- /internal/infrastructure/server/sse_connection_manager.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/FreePeak/cortex/internal/domain" 9 | ) 10 | 11 | // sseConnectionManager implements the domain.ConnectionManager interface 12 | // for managing SSE connections. 13 | type sseConnectionManager struct { 14 | mu sync.RWMutex 15 | sessions map[string]domain.SSESession 16 | } 17 | 18 | // NewSSEConnectionManager creates a new connection manager for SSE sessions. 19 | func NewSSEConnectionManager() domain.ConnectionManager { 20 | return &sseConnectionManager{ 21 | sessions: make(map[string]domain.SSESession), 22 | } 23 | } 24 | 25 | // AddSession adds a session to the connection manager. 26 | func (m *sseConnectionManager) AddSession(session domain.SSESession) { 27 | m.mu.Lock() 28 | defer m.mu.Unlock() 29 | m.sessions[session.ID()] = session 30 | } 31 | 32 | // RemoveSession removes a session from the connection manager. 33 | func (m *sseConnectionManager) RemoveSession(sessionID string) { 34 | m.mu.Lock() 35 | defer m.mu.Unlock() 36 | delete(m.sessions, sessionID) 37 | } 38 | 39 | // GetSession retrieves a session by its ID. 40 | func (m *sseConnectionManager) GetSession(sessionID string) (domain.SSESession, bool) { 41 | m.mu.RLock() 42 | defer m.mu.RUnlock() 43 | session, ok := m.sessions[sessionID] 44 | return session, ok 45 | } 46 | 47 | // Broadcast sends an event to all connected sessions. 48 | func (m *sseConnectionManager) Broadcast(event interface{}) error { 49 | // Format the event as a SSE message 50 | eventData, err := json.Marshal(event) 51 | if err != nil { 52 | return fmt.Errorf("failed to marshal event: %w", err) 53 | } 54 | 55 | eventStr := fmt.Sprintf("event: message\ndata: %s\n\n", eventData) 56 | 57 | m.mu.RLock() 58 | defer m.mu.RUnlock() 59 | 60 | for _, session := range m.sessions { 61 | select { 62 | case session.NotificationChannel() <- eventStr: 63 | // Event sent successfully 64 | default: 65 | // Queue is full or closed, we continue anyway to avoid blocking 66 | } 67 | } 68 | 69 | return nil 70 | } 71 | 72 | // CloseAll closes all active sessions. 73 | func (m *sseConnectionManager) CloseAll() { 74 | m.mu.Lock() 75 | defer m.mu.Unlock() 76 | 77 | for _, session := range m.sessions { 78 | session.Close() 79 | } 80 | 81 | // Clear the map 82 | m.sessions = make(map[string]domain.SSESession) 83 | } 84 | 85 | // Count returns the number of active sessions. 86 | func (m *sseConnectionManager) Count() int { 87 | m.mu.RLock() 88 | defer m.mu.RUnlock() 89 | return len(m.sessions) 90 | } 91 | -------------------------------------------------------------------------------- /internal/infrastructure/server/sse_session.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/FreePeak/cortex/internal/domain" 10 | "github.com/google/uuid" 11 | ) 12 | 13 | // sse_session.go defines the implementation of the domain.SSESession interface. 14 | 15 | // sseSession2 represents an active SSE connection and implements the domain.SSESession interface. 16 | type sseSession2 struct { 17 | writer http.ResponseWriter 18 | flusher http.Flusher 19 | done chan struct{} 20 | eventQueue chan string // Channel for queuing events 21 | id string 22 | notifChan NotificationChannel 23 | ctx context.Context 24 | cancel context.CancelFunc 25 | } 26 | 27 | // NewSSESession creates a new SSE session. 28 | func NewSSESession(w http.ResponseWriter, userAgent string, bufferSize int) (domain.SSESession, error) { 29 | // Check if the ResponseWriter supports flushing 30 | flusher, ok := w.(http.Flusher) 31 | if !ok { 32 | return nil, ErrResponseWriterNotFlusher 33 | } 34 | 35 | // Create a context with cancellation for this session 36 | ctx, cancel := context.WithCancel(context.Background()) 37 | 38 | // Generate a unique ID for this session 39 | sessionID := uuid.New().String() 40 | 41 | session := &sseSession2{ 42 | writer: w, 43 | flusher: flusher, 44 | done: make(chan struct{}), 45 | eventQueue: make(chan string, bufferSize), 46 | id: sessionID, 47 | notifChan: make(NotificationChannel, bufferSize), 48 | ctx: ctx, 49 | cancel: cancel, 50 | } 51 | 52 | return session, nil 53 | } 54 | 55 | // ID returns the session ID. 56 | func (s *sseSession2) ID() string { 57 | return s.id 58 | } 59 | 60 | // NotificationChannel returns the channel for sending notifications. 61 | func (s *sseSession2) NotificationChannel() chan<- string { 62 | return s.eventQueue 63 | } 64 | 65 | // Close closes the session. 66 | func (s *sseSession2) Close() { 67 | s.cancel() 68 | close(s.done) 69 | close(s.notifChan) 70 | // We intentionally don't close eventQueue here to avoid panic 71 | // when writing to a closed channel. It will be garbage collected 72 | // when the session object is no longer referenced. 73 | } 74 | 75 | // Start begins processing events for this session. 76 | // This method should be called in a separate goroutine. 77 | func (s *sseSession2) Start() { 78 | // Set headers for SSE 79 | s.writer.Header().Set("Content-Type", "text/event-stream") 80 | s.writer.Header().Set("Cache-Control", "no-cache") 81 | s.writer.Header().Set("Connection", "keep-alive") 82 | s.writer.Header().Set("Access-Control-Allow-Origin", "*") 83 | s.flusher.Flush() 84 | 85 | // Main event loop - processing events from the eventQueue 86 | for { 87 | select { 88 | case <-s.ctx.Done(): 89 | // Context canceled, stop processing 90 | return 91 | case <-s.done: 92 | // Session closed, stop processing 93 | return 94 | case event := <-s.eventQueue: 95 | // Write the event directly to the response 96 | _, _ = s.writer.Write([]byte(event)) 97 | s.flusher.Flush() 98 | case notification := <-s.notifChan: 99 | // Handle notifications from the MCP server 100 | // This is important to maintain compatibility with the original implementation 101 | // Converting notification to SSE event format 102 | event, err := json.Marshal(notification) 103 | if err == nil { 104 | _, _ = s.writer.Write([]byte(fmt.Sprintf("event: message\ndata: %s\n\n", event))) 105 | s.flusher.Flush() 106 | } 107 | } 108 | } 109 | } 110 | 111 | // Context returns the session's context. 112 | func (s *sseSession2) Context() context.Context { 113 | return s.ctx 114 | } 115 | -------------------------------------------------------------------------------- /internal/interfaces/stdio/options.go: -------------------------------------------------------------------------------- 1 | package stdio 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/FreePeak/cortex/internal/domain" 8 | ) 9 | 10 | // WithToolHandler registers a custom handler function for a specific tool. 11 | // This allows you to override the default tool handling behavior. 12 | func WithToolHandler(toolName string, handler func(ctx context.Context, params map[string]interface{}, session *domain.ClientSession) (interface{}, error)) StdioOption { 13 | return func(s *StdioServer) { 14 | if s.processor == nil { 15 | s.processor = NewMessageProcessor(s.server, s.logger) 16 | } 17 | 18 | // Create an adapter that converts our handler function to a MethodHandlerFunc 19 | adapter := MethodHandlerFunc(func(ctx context.Context, params interface{}, id interface{}) (interface{}, *domain.JSONRPCError) { 20 | // Extract tool parameters 21 | paramsMap, ok := params.(map[string]interface{}) 22 | if !ok { 23 | return nil, &domain.JSONRPCError{ 24 | Code: InvalidParamsCode, 25 | Message: "Invalid params", 26 | } 27 | } 28 | 29 | // Check if this is a call to our specific tool 30 | nameParam, ok := paramsMap["name"].(string) 31 | if !ok || nameParam != toolName { 32 | // Let the default handler handle other tools 33 | return nil, &domain.JSONRPCError{ 34 | Code: MethodNotFoundCode, 35 | Message: fmt.Sprintf("Tool handler mismatch: expected %s, got %s", toolName, nameParam), 36 | } 37 | } 38 | 39 | // Get tool parameters - check both parameters and arguments fields 40 | toolParams, ok := paramsMap["parameters"].(map[string]interface{}) 41 | if !ok { 42 | // Try arguments field if parameters is not available 43 | toolParams, ok = paramsMap["arguments"].(map[string]interface{}) 44 | if !ok { 45 | toolParams = map[string]interface{}{} 46 | } 47 | } 48 | 49 | // Create a dummy session for now 50 | session := &domain.ClientSession{ 51 | ID: "stdio-session", 52 | UserAgent: "stdio-client", 53 | Connected: true, 54 | } 55 | 56 | // Call the handler 57 | result, err := handler(ctx, toolParams, session) 58 | if err != nil { 59 | return nil, &domain.JSONRPCError{ 60 | Code: InternalErrorCode, 61 | Message: err.Error(), 62 | } 63 | } 64 | 65 | return result, nil 66 | }) 67 | 68 | // Override the tools/call handler with our custom one 69 | s.processor.RegisterHandler("tools/call", adapter) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/tools/tools.go: -------------------------------------------------------------------------------- 1 | // Package tools provides the tools infrastructure for the Cortex MCP platform. 2 | package tools 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/FreePeak/cortex/internal/domain" 8 | "github.com/FreePeak/cortex/internal/infrastructure/logging" 9 | ) 10 | 11 | // ToolRegistry provides registration methods for tool handlers 12 | type ToolRegistry interface { 13 | RegisterToolHandler(name string, handler func(ctx context.Context, params map[string]interface{}, session *domain.ClientSession) (interface{}, error)) 14 | } 15 | 16 | // ToolProvider is an interface for a service that provides tools 17 | type ToolProvider interface { 18 | // RegisterTool registers the tool with the provided registry 19 | RegisterTool(registerFunc func(string, func(context.Context, map[string]interface{}, *domain.ClientSession) (interface{}, error))) 20 | 21 | // GetToolDefinitions returns the tool definitions 22 | GetToolDefinitions() []*domain.Tool 23 | } 24 | 25 | // Manager manages all available tools 26 | type Manager struct { 27 | providers []ToolProvider 28 | logger *logging.Logger 29 | } 30 | 31 | // NewManager creates a new tools manager 32 | func NewManager(logger *logging.Logger) *Manager { 33 | if logger == nil { 34 | logger = logging.Default() 35 | } 36 | 37 | return &Manager{ 38 | providers: []ToolProvider{}, 39 | logger: logger, 40 | } 41 | } 42 | 43 | // RegisterProvider registers an external tool provider 44 | func (m *Manager) RegisterProvider(provider ToolProvider) { 45 | m.providers = append(m.providers, provider) 46 | } 47 | 48 | // GetAllTools returns all tool definitions from all providers 49 | func (m *Manager) GetAllTools() []*domain.Tool { 50 | var allTools []*domain.Tool 51 | 52 | for _, provider := range m.providers { 53 | allTools = append(allTools, provider.GetToolDefinitions()...) 54 | } 55 | 56 | return allTools 57 | } 58 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 92 | 93 | 94 | 100 | 101 | 102 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /logs/cortex-20250401-003412.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreePeak/cortex/64e47c1d4a73c1fe9fc2491c4ad49b9cd8c0aeb2/logs/cortex-20250401-003412.log -------------------------------------------------------------------------------- /pkg/README.md: -------------------------------------------------------------------------------- 1 | # MCP Server Platform for Go 2 | 3 | The Model Context Protocol (MCP) Server Platform for Go provides a simple way to create MCP-compliant servers in Go. This Platform allows you to: 4 | 5 | - Create MCP servers with custom tools 6 | - Handle tool calls with your own business logic 7 | - Serve MCP over standard I/O 8 | 9 | ## Usage 10 | 11 | ### Creating a simple MCP server 12 | 13 | ```go 14 | package main 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "log" 20 | "os" 21 | 22 | "github.com/FreePeak/cortex/pkg/server" 23 | "github.com/FreePeak/cortex/pkg/tools" 24 | ) 25 | 26 | func main() { 27 | // Create the server 28 | mcpServer := server.NewMCPServer("My MCP Server", "1.0.0") 29 | 30 | // Create a tool 31 | echoTool := tools.NewTool("echo", 32 | tools.WithDescription("Echoes back the input message"), 33 | tools.WithString("message", 34 | tools.Description("The message to echo back"), 35 | tools.Required(), 36 | ), 37 | ) 38 | 39 | // Add the tool to the server with a handler 40 | ctx := context.Background() 41 | err := mcpServer.AddTool(ctx, echoTool, handleEcho) 42 | if err != nil { 43 | log.Fatalf("Error adding tool: %v", err) 44 | } 45 | 46 | // Start the server over stdio 47 | if err := mcpServer.ServeStdio(); err != nil { 48 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 49 | os.Exit(1) 50 | } 51 | } 52 | 53 | // Echo tool handler 54 | func handleEcho(ctx context.Context, request server.ToolCallRequest) (interface{}, error) { 55 | // Extract the message parameter 56 | message, ok := request.Parameters["message"].(string) 57 | if !ok { 58 | return nil, fmt.Errorf("missing or invalid 'message' parameter") 59 | } 60 | 61 | // Return the echo response 62 | return map[string]interface{}{ 63 | "content": []map[string]interface{}{ 64 | { 65 | "type": "text", 66 | "text": message, 67 | }, 68 | }, 69 | }, nil 70 | } 71 | ``` 72 | 73 | ### Creating Tools 74 | 75 | The Platform provides a fluent interface for creating tools and their parameters: 76 | 77 | ```go 78 | // Create a calculator tool 79 | calculatorTool := tools.NewTool("calculator", 80 | tools.WithDescription("Performs basic arithmetic operations"), 81 | tools.WithString("operation", 82 | tools.Description("The operation to perform (add, subtract, multiply, divide)"), 83 | tools.Required(), 84 | ), 85 | tools.WithNumber("a", 86 | tools.Description("First number"), 87 | tools.Required(), 88 | ), 89 | tools.WithNumber("b", 90 | tools.Description("Second number"), 91 | tools.Required(), 92 | ), 93 | ) 94 | ``` 95 | 96 | ### Handling Tool Calls 97 | 98 | Tool handlers receive a `ToolCallRequest` and return a result or an error: 99 | 100 | ```go 101 | func handleCalculator(ctx context.Context, request server.ToolCallRequest) (interface{}, error) { 102 | // Extract parameters 103 | operation, ok := request.Parameters["operation"].(string) 104 | if !ok { 105 | return nil, fmt.Errorf("missing or invalid 'operation' parameter") 106 | } 107 | 108 | a, ok := request.Parameters["a"].(float64) 109 | if !ok { 110 | return nil, fmt.Errorf("missing or invalid 'a' parameter") 111 | } 112 | 113 | b, ok := request.Parameters["b"].(float64) 114 | if !ok { 115 | return nil, fmt.Errorf("missing or invalid 'b' parameter") 116 | } 117 | 118 | // Perform the calculation 119 | var result float64 120 | switch operation { 121 | case "add": 122 | result = a + b 123 | case "subtract": 124 | result = a - b 125 | case "multiply": 126 | result = a * b 127 | case "divide": 128 | if b == 0 { 129 | return nil, fmt.Errorf("division by zero") 130 | } 131 | result = a / b 132 | default: 133 | return nil, fmt.Errorf("unknown operation: %s", operation) 134 | } 135 | 136 | // Return the result 137 | return map[string]interface{}{ 138 | "content": []map[string]interface{}{ 139 | { 140 | "type": "text", 141 | "text": fmt.Sprintf("Result: %v", result), 142 | }, 143 | }, 144 | }, nil 145 | } 146 | ``` 147 | 148 | ## Package Structure 149 | 150 | The Platform consists of several packages: 151 | 152 | - `pkg/server`: Core server implementation 153 | - `pkg/tools`: Utilities for creating and configuring tools 154 | - `pkg/types`: Common types and interfaces 155 | 156 | ## Examples 157 | 158 | Check out the `examples` directory for complete working examples. -------------------------------------------------------------------------------- /pkg/builder/tool_handler.go: -------------------------------------------------------------------------------- 1 | package builder 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/FreePeak/cortex/internal/domain" 7 | "github.com/FreePeak/cortex/internal/interfaces/stdio" 8 | ) 9 | 10 | // ToolHandlerFunc is an adapter function for handling tool calls in the MCP server. 11 | // This wraps a custom handler function in the format expected by the internal APIs. 12 | type ToolHandlerFunc func(ctx context.Context, params map[string]interface{}, session *domain.ClientSession) (interface{}, error) 13 | 14 | // WithToolHandler returns a stdio option to handle a specific tool. 15 | // This allows you to register custom handlers for tools. 16 | func WithToolHandler(toolName string, handler ToolHandlerFunc) stdio.StdioOption { 17 | return stdio.WithToolHandler(toolName, handler) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/integration/pocketbase/plugin_test.go: -------------------------------------------------------------------------------- 1 | package pocketbase 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | 11 | "github.com/FreePeak/cortex/pkg/tools" 12 | ) 13 | 14 | func TestNewCortexPlugin(t *testing.T) { 15 | // Create a plugin with default options 16 | plugin := NewCortexPlugin() 17 | 18 | // Check default values 19 | assert.Equal(t, "Cortex MCP Server", plugin.name) 20 | assert.Equal(t, "1.0.0", plugin.version) 21 | assert.Equal(t, "/api/mcp", plugin.basePath) 22 | assert.NotNil(t, plugin.logger) 23 | assert.NotNil(t, plugin.mcpServer) 24 | } 25 | 26 | func TestWithOptions(t *testing.T) { 27 | // Create a custom logger 28 | logger := log.New(os.Stderr, "[test] ", log.LstdFlags) 29 | 30 | // Create a plugin with custom options 31 | plugin := NewCortexPlugin( 32 | WithName("Test Server"), 33 | WithVersion("2.0.0"), 34 | WithBasePath("/test"), 35 | WithLogger(logger), 36 | ) 37 | 38 | // Check that options were applied 39 | assert.Equal(t, "Test Server", plugin.name) 40 | assert.Equal(t, "2.0.0", plugin.version) 41 | assert.Equal(t, "/test", plugin.basePath) 42 | assert.Equal(t, logger, plugin.logger) 43 | } 44 | 45 | func TestAddTool(t *testing.T) { 46 | // Create a plugin 47 | plugin := NewCortexPlugin() 48 | 49 | // Create a tool 50 | echoTool := tools.NewTool("echo", 51 | tools.WithDescription("Echoes back the input message"), 52 | tools.WithString("message", 53 | tools.Description("The message to echo back"), 54 | tools.Required(), 55 | ), 56 | ) 57 | 58 | // Add the tool 59 | err := plugin.AddTool(echoTool, func(ctx context.Context, request ToolCallRequest) (interface{}, error) { 60 | message := request.Parameters["message"].(string) 61 | return map[string]interface{}{ 62 | "content": []map[string]interface{}{ 63 | { 64 | "type": "text", 65 | "text": message, 66 | }, 67 | }, 68 | }, nil 69 | }) 70 | 71 | // Check that there was no error 72 | assert.NoError(t, err) 73 | } 74 | 75 | func TestGetServerInfo(t *testing.T) { 76 | // Create a plugin with custom name and version 77 | plugin := NewCortexPlugin( 78 | WithName("Test Server"), 79 | WithVersion("2.0.0"), 80 | ) 81 | 82 | // Get server info 83 | info := plugin.GetServerInfo() 84 | 85 | // Check info values 86 | assert.Equal(t, "Test Server", info.Name) 87 | assert.Equal(t, "2.0.0", info.Version) 88 | assert.NotEmpty(t, info.Address) 89 | } 90 | -------------------------------------------------------------------------------- /pkg/plugin/README.md: -------------------------------------------------------------------------------- 1 | # Cortex Plugin System 2 | 3 | The Cortex Plugin System provides a flexible architecture for extending the Cortex MCP (Model Context Protocol) server with external tools and services. This system allows third-party providers to register tools that can be called through the MCP protocol. 4 | 5 | ## Overview 6 | 7 | The plugin system includes the following key components: 8 | 9 | - **Provider Interface**: Defines the contract that tool providers must implement 10 | - **Registry**: Manages the registration and discovery of providers and tools 11 | - **Base Provider**: Provides a foundation for building tool providers 12 | 13 | ## Provider Interface 14 | 15 | Tool providers must implement the `Provider` interface: 16 | 17 | ```go 18 | type Provider interface { 19 | // GetProviderInfo returns information about the tool provider 20 | GetProviderInfo(ctx context.Context) (*ProviderInfo, error) 21 | 22 | // GetTools returns a list of tools provided by this provider 23 | GetTools(ctx context.Context) ([]*types.Tool, error) 24 | 25 | // ExecuteTool executes a specific tool with the given parameters 26 | ExecuteTool(ctx context.Context, request *ExecuteRequest) (*ExecuteResponse, error) 27 | } 28 | ``` 29 | 30 | ## Creating a Tool Provider 31 | 32 | The easiest way to create a tool provider is to use the `BaseProvider` implementation: 33 | 34 | ```go 35 | // Create provider info 36 | info := plugin.ProviderInfo{ 37 | ID: "my-provider", 38 | Name: "My Provider", 39 | Version: "1.0.0", 40 | Description: "A custom tool provider", 41 | Author: "Your Name", 42 | URL: "https://github.com/yourusername/your-repo", 43 | } 44 | 45 | // Create base provider 46 | baseProvider := plugin.NewBaseProvider(info, logger) 47 | 48 | // Create your custom provider 49 | myProvider := &MyProvider{ 50 | BaseProvider: baseProvider, 51 | // Add your custom fields here 52 | } 53 | 54 | // Register tools with your provider 55 | myTool := tools.NewTool("my-tool", 56 | tools.WithDescription("A custom tool"), 57 | tools.WithString("param1", tools.Description("Parameter 1"), tools.Required()), 58 | ) 59 | 60 | // Register the tool with your provider 61 | myProvider.RegisterTool(myTool, handleMyTool) 62 | ``` 63 | 64 | ## Tool Handler Function 65 | 66 | Each tool needs a handler function that follows this signature: 67 | 68 | ```go 69 | func handleMyTool(ctx context.Context, params map[string]interface{}, session *types.ClientSession) (interface{}, error) { 70 | // Extract parameters 71 | param1, ok := params["param1"].(string) 72 | if !ok { 73 | return nil, fmt.Errorf("missing or invalid 'param1' parameter") 74 | } 75 | 76 | // Process the request 77 | result := fmt.Sprintf("Processed param1: %s", param1) 78 | 79 | // Return the result in the format expected by the MCP protocol 80 | return map[string]interface{}{ 81 | "content": []map[string]interface{}{ 82 | { 83 | "type": "text", 84 | "text": result, 85 | }, 86 | }, 87 | }, nil 88 | } 89 | ``` 90 | 91 | ## Using the Registry 92 | 93 | The registry manages the providers and tools: 94 | 95 | ```go 96 | // Create a registry 97 | registry := plugin.NewRegistry(logger) 98 | 99 | // Register a provider 100 | registry.RegisterProvider(ctx, myProvider) 101 | 102 | // Get a tool 103 | tool, provider, err := registry.GetTool(ctx, "my-tool") 104 | if err != nil { 105 | // Handle error 106 | } 107 | 108 | // List all tools 109 | tools, err := registry.ListTools(ctx) 110 | if err != nil { 111 | // Handle error 112 | } 113 | ``` 114 | 115 | ## Using the Flexible MCP Server 116 | 117 | The `FlexibleMCPServer` supports dynamic tool providers: 118 | 119 | ```go 120 | // Create a registry 121 | registry := plugin.NewRegistry(logger) 122 | 123 | // Create the flexible MCP server 124 | mcpServer := server.NewFlexibleMCPServer("My MCP Server", "1.0.0", registry, logger) 125 | 126 | // Register providers with the server 127 | mcpServer.RegisterProvider(ctx, myProvider) 128 | 129 | // Start the server (stdio or HTTP) 130 | mcpServer.ServeStdio() 131 | // or 132 | mcpServer.ServeHTTP() 133 | ``` 134 | 135 | ## Example Providers 136 | 137 | The Cortex project includes example providers in the `examples/providers/` directory: 138 | 139 | - **Weather Provider**: Provides tools for getting weather forecasts 140 | - **Database Provider**: Provides tools for simple database operations 141 | 142 | These examples demonstrate how to create providers that integrate with the Cortex platform. 143 | 144 | ## Security Considerations 145 | 146 | When implementing tool providers, consider the following security best practices: 147 | 148 | 1. Validate all input parameters thoroughly 149 | 2. Limit access to sensitive operations 150 | 3. Use context for cancellation and timeouts 151 | 4. Log security-relevant events 152 | 5. Avoid exposing internal details in error messages -------------------------------------------------------------------------------- /pkg/plugin/base_provider.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/FreePeak/cortex/pkg/types" 9 | ) 10 | 11 | // ToolExecutor is a function that executes a tool and returns a result. 12 | type ToolExecutor func(ctx context.Context, params map[string]interface{}, session *types.ClientSession) (interface{}, error) 13 | 14 | // BaseProvider implements the Provider interface and provides a foundation for building tool providers. 15 | type BaseProvider struct { 16 | info ProviderInfo 17 | tools []*types.Tool 18 | executors map[string]ToolExecutor 19 | logger *log.Logger 20 | } 21 | 22 | // NewBaseProvider creates a new BaseProvider with the given info. 23 | func NewBaseProvider(info ProviderInfo, logger *log.Logger) *BaseProvider { 24 | if logger == nil { 25 | logger = log.Default() 26 | } 27 | 28 | return &BaseProvider{ 29 | info: info, 30 | tools: make([]*types.Tool, 0), 31 | executors: make(map[string]ToolExecutor), 32 | logger: logger, 33 | } 34 | } 35 | 36 | // GetProviderInfo returns information about the tool provider. 37 | func (p *BaseProvider) GetProviderInfo(ctx context.Context) (*ProviderInfo, error) { 38 | return &p.info, nil 39 | } 40 | 41 | // GetTools returns a list of tools provided by this provider. 42 | func (p *BaseProvider) GetTools(ctx context.Context) ([]*types.Tool, error) { 43 | return p.tools, nil 44 | } 45 | 46 | // ExecuteTool executes a specific tool with the given parameters. 47 | func (p *BaseProvider) ExecuteTool(ctx context.Context, request *ExecuteRequest) (*ExecuteResponse, error) { 48 | // Validate the request 49 | if request == nil { 50 | return nil, fmt.Errorf("request cannot be nil") 51 | } 52 | 53 | if request.ToolName == "" { 54 | return nil, fmt.Errorf("tool name cannot be empty") 55 | } 56 | 57 | // Get the executor for the tool 58 | executor, exists := p.executors[request.ToolName] 59 | if !exists { 60 | return nil, fmt.Errorf("tool %s not found", request.ToolName) 61 | } 62 | 63 | // Execute the tool 64 | p.logger.Printf("Executing tool: %s", request.ToolName) 65 | result, err := executor(ctx, request.Parameters, request.Session) 66 | if err != nil { 67 | p.logger.Printf("Error executing tool %s: %v", request.ToolName, err) 68 | return &ExecuteResponse{Error: err}, nil 69 | } 70 | 71 | // Return the result 72 | return &ExecuteResponse{Content: result}, nil 73 | } 74 | 75 | // RegisterTool registers a new tool with the provider. 76 | func (p *BaseProvider) RegisterTool(tool *types.Tool, executor ToolExecutor) error { 77 | if tool == nil { 78 | return fmt.Errorf("tool cannot be nil") 79 | } 80 | 81 | if executor == nil { 82 | return fmt.Errorf("executor cannot be nil") 83 | } 84 | 85 | if tool.Name == "" { 86 | return fmt.Errorf("tool name cannot be empty") 87 | } 88 | 89 | // Check if the tool already exists 90 | for _, existingTool := range p.tools { 91 | if existingTool.Name == tool.Name { 92 | return fmt.Errorf("tool %s already registered", tool.Name) 93 | } 94 | } 95 | 96 | // Add the tool and its executor 97 | p.tools = append(p.tools, tool) 98 | p.executors[tool.Name] = executor 99 | p.logger.Printf("Registered tool %s with provider %s", tool.Name, p.info.ID) 100 | 101 | return nil 102 | } 103 | 104 | // UnregisterTool removes a tool from the provider. 105 | func (p *BaseProvider) UnregisterTool(toolName string) error { 106 | // Find the tool 107 | index := -1 108 | for i, tool := range p.tools { 109 | if tool.Name == toolName { 110 | index = i 111 | break 112 | } 113 | } 114 | 115 | if index == -1 { 116 | return fmt.Errorf("tool %s not found", toolName) 117 | } 118 | 119 | // Remove the tool 120 | p.tools = append(p.tools[:index], p.tools[index+1:]...) 121 | delete(p.executors, toolName) 122 | p.logger.Printf("Unregistered tool %s from provider %s", toolName, p.info.ID) 123 | 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /pkg/plugin/interface.go: -------------------------------------------------------------------------------- 1 | // Package plugin defines interfaces and utilities for the Cortex plugin system. 2 | package plugin 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/FreePeak/cortex/pkg/types" 8 | ) 9 | 10 | // Provider represents a tool provider that can register tools with the Cortex platform. 11 | type Provider interface { 12 | // GetProviderInfo returns information about the tool provider. 13 | GetProviderInfo(ctx context.Context) (*ProviderInfo, error) 14 | 15 | // GetTools returns a list of tools provided by this provider. 16 | GetTools(ctx context.Context) ([]*types.Tool, error) 17 | 18 | // ExecuteTool executes a specific tool with the given parameters. 19 | ExecuteTool(ctx context.Context, request *ExecuteRequest) (*ExecuteResponse, error) 20 | } 21 | 22 | // ProviderInfo contains metadata about a tool provider. 23 | type ProviderInfo struct { 24 | ID string 25 | Name string 26 | Version string 27 | Description string 28 | Author string 29 | URL string 30 | } 31 | 32 | // ExecuteRequest contains information for executing a tool. 33 | type ExecuteRequest struct { 34 | ToolName string 35 | Parameters map[string]interface{} 36 | Session *types.ClientSession 37 | } 38 | 39 | // ExecuteResponse contains the result of executing a tool. 40 | type ExecuteResponse struct { 41 | Content interface{} 42 | Error error 43 | } 44 | 45 | // Registry manages the registration and discovery of tool providers. 46 | type Registry interface { 47 | // RegisterProvider registers a new tool provider with the registry. 48 | RegisterProvider(ctx context.Context, provider Provider) error 49 | 50 | // UnregisterProvider removes a tool provider from the registry. 51 | UnregisterProvider(ctx context.Context, providerID string) error 52 | 53 | // GetProvider retrieves a specific provider by ID. 54 | GetProvider(ctx context.Context, providerID string) (Provider, error) 55 | 56 | // ListProviders returns all registered providers. 57 | ListProviders(ctx context.Context) ([]Provider, error) 58 | 59 | // GetTool retrieves a specific tool by name. 60 | GetTool(ctx context.Context, toolName string) (*types.Tool, Provider, error) 61 | 62 | // ListTools returns all tools from all registered providers. 63 | ListTools(ctx context.Context) ([]*types.Tool, error) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/plugin/registry.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "sync" 8 | 9 | "github.com/FreePeak/cortex/pkg/types" 10 | ) 11 | 12 | // DefaultRegistry is the default implementation of the Registry interface. 13 | type DefaultRegistry struct { 14 | providers map[string]Provider 15 | toolMap map[string]string // Maps tool names to provider IDs 16 | mu sync.RWMutex 17 | logger *log.Logger 18 | } 19 | 20 | // NewRegistry creates a new registry for managing tool providers. 21 | func NewRegistry(logger *log.Logger) *DefaultRegistry { 22 | if logger == nil { 23 | logger = log.Default() 24 | } 25 | 26 | return &DefaultRegistry{ 27 | providers: make(map[string]Provider), 28 | toolMap: make(map[string]string), 29 | logger: logger, 30 | } 31 | } 32 | 33 | // RegisterProvider registers a new tool provider with the registry. 34 | func (r *DefaultRegistry) RegisterProvider(ctx context.Context, provider Provider) error { 35 | if provider == nil { 36 | return fmt.Errorf("provider cannot be nil") 37 | } 38 | 39 | info, err := provider.GetProviderInfo(ctx) 40 | if err != nil { 41 | return fmt.Errorf("failed to get provider info: %w", err) 42 | } 43 | 44 | if info.ID == "" { 45 | return fmt.Errorf("provider ID cannot be empty") 46 | } 47 | 48 | // Register the provider 49 | r.mu.Lock() 50 | defer r.mu.Unlock() 51 | 52 | // Check if provider already exists 53 | if _, exists := r.providers[info.ID]; exists { 54 | return fmt.Errorf("provider with ID %s is already registered", info.ID) 55 | } 56 | 57 | // Add provider to registry 58 | r.providers[info.ID] = provider 59 | r.logger.Printf("Registered provider: %s (%s)", info.Name, info.ID) 60 | 61 | // Register all tools provided by this provider 62 | tools, err := provider.GetTools(ctx) 63 | if err != nil { 64 | // We registered the provider but failed to get tools 65 | // Let's keep the provider registered but log the error 66 | r.logger.Printf("Error getting tools from provider %s: %v", info.ID, err) 67 | return nil 68 | } 69 | 70 | // Register all tools with this provider 71 | for _, tool := range tools { 72 | if tool.Name == "" { 73 | r.logger.Printf("Skipping tool with empty name from provider %s", info.ID) 74 | continue 75 | } 76 | 77 | // Check for tool name collision 78 | if existingProvider, exists := r.toolMap[tool.Name]; exists { 79 | r.logger.Printf("Tool name collision: %s already registered by provider %s", tool.Name, existingProvider) 80 | continue 81 | } 82 | 83 | r.toolMap[tool.Name] = info.ID 84 | r.logger.Printf("Registered tool: %s from provider %s", tool.Name, info.ID) 85 | } 86 | 87 | return nil 88 | } 89 | 90 | // UnregisterProvider removes a tool provider from the registry. 91 | func (r *DefaultRegistry) UnregisterProvider(ctx context.Context, providerID string) error { 92 | r.mu.Lock() 93 | defer r.mu.Unlock() 94 | 95 | // Check if provider exists 96 | if _, exists := r.providers[providerID]; !exists { 97 | return fmt.Errorf("provider with ID %s is not registered", providerID) 98 | } 99 | 100 | // Remove all tools associated with this provider 101 | var toolsToRemove []string 102 | for toolName, id := range r.toolMap { 103 | if id == providerID { 104 | toolsToRemove = append(toolsToRemove, toolName) 105 | } 106 | } 107 | 108 | for _, toolName := range toolsToRemove { 109 | delete(r.toolMap, toolName) 110 | } 111 | 112 | // Remove the provider 113 | delete(r.providers, providerID) 114 | r.logger.Printf("Unregistered provider: %s with %d tools", providerID, len(toolsToRemove)) 115 | 116 | return nil 117 | } 118 | 119 | // GetProvider retrieves a specific provider by ID. 120 | func (r *DefaultRegistry) GetProvider(ctx context.Context, providerID string) (Provider, error) { 121 | r.mu.RLock() 122 | defer r.mu.RUnlock() 123 | 124 | provider, exists := r.providers[providerID] 125 | if !exists { 126 | return nil, fmt.Errorf("provider with ID %s is not registered", providerID) 127 | } 128 | 129 | return provider, nil 130 | } 131 | 132 | // ListProviders returns all registered providers. 133 | func (r *DefaultRegistry) ListProviders(ctx context.Context) ([]Provider, error) { 134 | r.mu.RLock() 135 | defer r.mu.RUnlock() 136 | 137 | providers := make([]Provider, 0, len(r.providers)) 138 | for _, provider := range r.providers { 139 | providers = append(providers, provider) 140 | } 141 | 142 | return providers, nil 143 | } 144 | 145 | // GetTool retrieves a specific tool by name. 146 | func (r *DefaultRegistry) GetTool(ctx context.Context, toolName string) (*types.Tool, Provider, error) { 147 | r.mu.RLock() 148 | defer r.mu.RUnlock() 149 | 150 | // Find the provider for this tool 151 | providerID, exists := r.toolMap[toolName] 152 | if !exists { 153 | return nil, nil, fmt.Errorf("tool %s is not registered", toolName) 154 | } 155 | 156 | // Get the provider 157 | provider, exists := r.providers[providerID] 158 | if !exists { 159 | // This should not happen, but handle it anyway 160 | return nil, nil, fmt.Errorf("provider for tool %s is no longer registered", toolName) 161 | } 162 | 163 | // Get all tools from the provider 164 | tools, err := provider.GetTools(ctx) 165 | if err != nil { 166 | return nil, nil, fmt.Errorf("failed to get tools from provider %s: %w", providerID, err) 167 | } 168 | 169 | // Find the specific tool 170 | for _, tool := range tools { 171 | if tool.Name == toolName { 172 | return tool, provider, nil 173 | } 174 | } 175 | 176 | // Tool was registered but not found in provider's tools 177 | return nil, nil, fmt.Errorf("tool %s is no longer provided by provider %s", toolName, providerID) 178 | } 179 | 180 | // ListTools returns all tools from all registered providers. 181 | func (r *DefaultRegistry) ListTools(ctx context.Context) ([]*types.Tool, error) { 182 | r.mu.RLock() 183 | defer r.mu.RUnlock() 184 | 185 | var allTools []*types.Tool 186 | 187 | for providerID, provider := range r.providers { 188 | tools, err := provider.GetTools(ctx) 189 | if err != nil { 190 | r.logger.Printf("Error getting tools from provider %s: %v", providerID, err) 191 | continue 192 | } 193 | 194 | allTools = append(allTools, tools...) 195 | } 196 | 197 | return allTools, nil 198 | } 199 | -------------------------------------------------------------------------------- /pkg/server/embeddable.go: -------------------------------------------------------------------------------- 1 | // Package server provides the MCP server implementation. 2 | package server 3 | 4 | import ( 5 | "context" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/FreePeak/cortex/pkg/types" 10 | ) 11 | 12 | // ServerInfo contains basic information about the MCP server 13 | type ServerInfo struct { 14 | // Name is the name of the server 15 | Name string 16 | 17 | // Version is the version of the server 18 | Version string 19 | 20 | // Address is the address the server is listening on 21 | Address string 22 | } 23 | 24 | // Embeddable defines an interface for MCP servers that can be embedded 25 | // in other applications like PocketBase, standard HTTP servers, etc. 26 | type Embeddable interface { 27 | // ToHTTPHandler returns an http.Handler that can be used to integrate 28 | // the MCP server with any standard Go HTTP server 29 | ToHTTPHandler() http.Handler 30 | 31 | // AddTool adds a tool to the MCP server 32 | AddTool(ctx context.Context, tool *types.Tool, handler ToolHandler) error 33 | 34 | // GetServerInfo returns basic information about the server 35 | GetServerInfo() ServerInfo 36 | 37 | // RegisterSession registers a new client session with the server 38 | // This allows sending messages to specific clients 39 | RegisterSession(sessionID string, userAgent string, callback func([]byte) error) error 40 | 41 | // UnregisterSession removes a client session from the server 42 | UnregisterSession(sessionID string) error 43 | 44 | // ExecuteTool executes a tool with the given request 45 | ExecuteTool(ctx context.Context, request ToolCallRequest) (interface{}, error) 46 | 47 | // SendToSession sends a message to a specific session 48 | SendToSession(sessionID string, message []byte) error 49 | 50 | // GetTools returns all registered tools 51 | GetTools() map[string]*types.Tool 52 | } 53 | 54 | // Ensure MCPServer implements Embeddable 55 | var _ Embeddable = (*MCPServer)(nil) 56 | 57 | // SetEmbedMode configures the MCPServer to work in embedded mode, 58 | // which prevents it from binding to a port when ToHTTPHandler is called. 59 | func (s *MCPServer) SetEmbedMode(embed bool) { 60 | s.embedMode = embed 61 | } 62 | 63 | // ToHTTPHandler returns an http.Handler for the MCP server 64 | func (s *MCPServer) ToHTTPHandler() http.Handler { 65 | // When in embed mode, we provide a simpler handler that doesn't try to bind to a port 66 | if s.embedMode { 67 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 68 | // Simple server info endpoint 69 | if r.URL.Path == "/sse" || r.URL.Path == "/events" { 70 | // For SSE endpoints, send a simple event stream response 71 | w.Header().Set("Content-Type", "text/event-stream") 72 | w.Header().Set("Cache-Control", "no-cache") 73 | w.Header().Set("Connection", "keep-alive") 74 | 75 | // Send the server info - using JSON-RPC 2.0 format 76 | serverInfo := `{"jsonrpc":"2.0","result":{"name":"` + s.name + `","version":"` + s.version + `","status":"running"},"id":"server.info"}` 77 | _, err := w.Write([]byte("event: server\ndata: " + serverInfo + "\n\n")) 78 | if err != nil { 79 | log.Printf("Error writing SSE server info: %v", err) 80 | return 81 | } 82 | 83 | // Keep the connection open 84 | <-r.Context().Done() 85 | return 86 | } 87 | 88 | if r.URL.Path == "/status" || r.URL.Path == "/" { 89 | // Return JSON-RPC 2.0 formatted server info for status requests 90 | w.Header().Set("Content-Type", "application/json") 91 | jsonResponse := `{"jsonrpc":"2.0","result":{"name":"` + s.name + `","version":"` + s.version + `","status":"running"},"id":"server.info"}` 92 | _, err := w.Write([]byte(jsonResponse)) 93 | if err != nil { 94 | log.Printf("Error writing status response: %v", err) 95 | return 96 | } 97 | return 98 | } 99 | 100 | // For all other requests, return a 404 for now - we need a more sophisticated solution 101 | http.NotFound(w, r) 102 | }) 103 | } 104 | 105 | // If not in embed mode, use the default implementation (which may try to start the server) 106 | // Create a handler that delegates to the MCP server's internal handlers without starting the server 107 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 108 | // Pass the request to the MCP server's handlers 109 | // This is a fallback but will likely not work correctly since the server isn't started 110 | http.Error(w, "The MCP server is not configured for embedding", http.StatusInternalServerError) 111 | }) 112 | } 113 | 114 | // GetServerInfo returns basic information about the server 115 | func (s *MCPServer) GetServerInfo() ServerInfo { 116 | // GetAddress() may return the default value (:8080), but we want to provide 117 | // the address that reflects what the server will actually use when embedded 118 | return ServerInfo{ 119 | Name: s.name, 120 | Version: s.version, 121 | Address: s.GetAddress(), 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /pkg/server/embeddable_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/mock" 10 | 11 | "github.com/FreePeak/cortex/pkg/types" 12 | ) 13 | 14 | // MockEmbeddable is a mock implementation of the Embeddable interface for testing 15 | type MockEmbeddable struct { 16 | mock.Mock 17 | } 18 | 19 | func (m *MockEmbeddable) ToHTTPHandler() http.Handler { 20 | args := m.Called() 21 | return args.Get(0).(http.Handler) 22 | } 23 | 24 | func (m *MockEmbeddable) AddTool(ctx context.Context, tool *types.Tool, handler ToolHandler) error { 25 | args := m.Called(ctx, tool, handler) 26 | return args.Error(0) 27 | } 28 | 29 | func (m *MockEmbeddable) GetServerInfo() ServerInfo { 30 | args := m.Called() 31 | return args.Get(0).(ServerInfo) 32 | } 33 | 34 | func TestEmbeddableInterface(t *testing.T) { 35 | // This test verifies that MCPServer implements the Embeddable interface 36 | var _ Embeddable = (*MCPServer)(nil) 37 | } 38 | 39 | func TestToHTTPHandler(t *testing.T) { 40 | mockEmb := new(MockEmbeddable) 41 | mockHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) 42 | 43 | mockEmb.On("ToHTTPHandler").Return(mockHandler) 44 | 45 | handler := mockEmb.ToHTTPHandler() 46 | 47 | assert.NotNil(t, handler) 48 | mockEmb.AssertExpectations(t) 49 | } 50 | 51 | func TestAddTool(t *testing.T) { 52 | mockEmb := new(MockEmbeddable) 53 | ctx := context.Background() 54 | mockTool := &types.Tool{ 55 | Name: "test-tool", 56 | Description: "Test tool", 57 | } 58 | 59 | mockHandler := func(ctx context.Context, request ToolCallRequest) (interface{}, error) { 60 | return nil, nil 61 | } 62 | 63 | mockEmb.On("AddTool", ctx, mockTool, mock.AnythingOfType("ToolHandler")).Return(nil) 64 | 65 | err := mockEmb.AddTool(ctx, mockTool, mockHandler) 66 | 67 | assert.NoError(t, err) 68 | mockEmb.AssertExpectations(t) 69 | } 70 | 71 | func TestGetServerInfo(t *testing.T) { 72 | mockEmb := new(MockEmbeddable) 73 | expectedInfo := ServerInfo{ 74 | Name: "Test Server", 75 | Version: "1.0.0", 76 | Address: "localhost:8080", 77 | } 78 | 79 | mockEmb.On("GetServerInfo").Return(expectedInfo) 80 | 81 | info := mockEmb.GetServerInfo() 82 | 83 | assert.Equal(t, expectedInfo, info) 84 | mockEmb.AssertExpectations(t) 85 | } 86 | -------------------------------------------------------------------------------- /pkg/server/http_adapter.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | ) 7 | 8 | // HTTPAdapterOption is a function that configures an HTTP adapter. 9 | type HTTPAdapterOption func(*HTTPAdapter) 10 | 11 | // WithPath sets the base path for the HTTP adapter. 12 | func WithPath(basePath string) HTTPAdapterOption { 13 | return func(a *HTTPAdapter) { 14 | a.basePath = basePath 15 | } 16 | } 17 | 18 | // HTTPAdapter adapts an Embeddable to be used in an HTTP server. 19 | type HTTPAdapter struct { 20 | embeddable Embeddable 21 | basePath string 22 | } 23 | 24 | // NewHTTPAdapter creates a new HTTPAdapter. 25 | func NewHTTPAdapter(embeddable Embeddable, opts ...HTTPAdapterOption) *HTTPAdapter { 26 | adapter := &HTTPAdapter{ 27 | embeddable: embeddable, 28 | basePath: "/mcp", 29 | } 30 | 31 | // Apply options 32 | for _, opt := range opts { 33 | opt(adapter) 34 | } 35 | 36 | return adapter 37 | } 38 | 39 | // Handler returns an http.Handler that can be registered with an HTTP server. 40 | func (a *HTTPAdapter) Handler() http.Handler { 41 | // Create a router that handles MCP routes 42 | baseHandler := a.embeddable.ToHTTPHandler() 43 | 44 | // Return a handler that checks the path and delegates to the MCP handler 45 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 46 | // Check if the request path starts with the MCP base path 47 | if len(r.URL.Path) >= len(a.basePath) && r.URL.Path[:len(a.basePath)] == a.basePath { 48 | // Strip the base path from the request 49 | r2 := new(http.Request) 50 | *r2 = *r 51 | 52 | // Remove the base path 53 | r2.URL = new(url.URL) 54 | *r2.URL = *r.URL 55 | r2.URL.Path = r.URL.Path[len(a.basePath):] 56 | if r2.URL.Path == "" { 57 | r2.URL.Path = "/" 58 | } 59 | 60 | // Delegate to the MCP handler 61 | baseHandler.ServeHTTP(w, r2) 62 | return 63 | } 64 | 65 | // Not an MCP request, return 404 66 | http.NotFound(w, r) 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /pkg/tools/helper.go: -------------------------------------------------------------------------------- 1 | // Package tools provides utility functions for creating MCP tools. 2 | package tools 3 | 4 | import ( 5 | "github.com/FreePeak/cortex/pkg/types" 6 | ) 7 | 8 | // ToolOption is a function that configures a tool. 9 | type ToolOption func(*types.Tool) 10 | 11 | // NewTool creates a new MCP tool with the given name and options. 12 | func NewTool(name string, options ...ToolOption) *types.Tool { 13 | tool := &types.Tool{ 14 | Name: name, 15 | Parameters: []types.ToolParameter{}, 16 | } 17 | 18 | // Apply all options 19 | for _, option := range options { 20 | option(tool) 21 | } 22 | 23 | return tool 24 | } 25 | 26 | // WithDescription sets the description of a tool. 27 | func WithDescription(description string) ToolOption { 28 | return func(t *types.Tool) { 29 | t.Description = description 30 | } 31 | } 32 | 33 | // Parameter types 34 | 35 | // ParameterOption is a function that configures a parameter. 36 | type ParameterOption func(*types.ToolParameter) 37 | 38 | // Description sets the description of a parameter. 39 | func Description(description string) ParameterOption { 40 | return func(p *types.ToolParameter) { 41 | p.Description = description 42 | } 43 | } 44 | 45 | // Required marks a parameter as required. 46 | func Required() ParameterOption { 47 | return func(p *types.ToolParameter) { 48 | p.Required = true 49 | } 50 | } 51 | 52 | // Items sets the schema for items in an array parameter. 53 | func Items(schema map[string]interface{}) ParameterOption { 54 | return func(p *types.ToolParameter) { 55 | p.Items = schema 56 | } 57 | } 58 | 59 | // Type functions for creating parameters 60 | 61 | // WithString adds a string parameter to a tool. 62 | func WithString(name string, options ...ParameterOption) ToolOption { 63 | return func(t *types.Tool) { 64 | param := types.ToolParameter{ 65 | Name: name, 66 | Type: "string", 67 | } 68 | 69 | // Apply options 70 | for _, option := range options { 71 | option(¶m) 72 | } 73 | 74 | t.Parameters = append(t.Parameters, param) 75 | } 76 | } 77 | 78 | // WithNumber adds a number parameter to a tool. 79 | func WithNumber(name string, options ...ParameterOption) ToolOption { 80 | return func(t *types.Tool) { 81 | param := types.ToolParameter{ 82 | Name: name, 83 | Type: "number", 84 | } 85 | 86 | // Apply options 87 | for _, option := range options { 88 | option(¶m) 89 | } 90 | 91 | t.Parameters = append(t.Parameters, param) 92 | } 93 | } 94 | 95 | // WithBoolean adds a boolean parameter to a tool. 96 | func WithBoolean(name string, options ...ParameterOption) ToolOption { 97 | return func(t *types.Tool) { 98 | param := types.ToolParameter{ 99 | Name: name, 100 | Type: "boolean", 101 | } 102 | 103 | // Apply options 104 | for _, option := range options { 105 | option(¶m) 106 | } 107 | 108 | t.Parameters = append(t.Parameters, param) 109 | } 110 | } 111 | 112 | // WithArray adds an array parameter to a tool. 113 | func WithArray(name string, options ...ParameterOption) ToolOption { 114 | return func(t *types.Tool) { 115 | param := types.ToolParameter{ 116 | Name: name, 117 | Type: "array", 118 | } 119 | 120 | // Apply options 121 | for _, option := range options { 122 | option(¶m) 123 | } 124 | 125 | t.Parameters = append(t.Parameters, param) 126 | } 127 | } 128 | 129 | // WithObject adds an object parameter to a tool. 130 | func WithObject(name string, options ...ParameterOption) ToolOption { 131 | return func(t *types.Tool) { 132 | param := types.ToolParameter{ 133 | Name: name, 134 | Type: "object", 135 | } 136 | 137 | // Apply options 138 | for _, option := range options { 139 | option(¶m) 140 | } 141 | 142 | t.Parameters = append(t.Parameters, param) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /pkg/types/types.go: -------------------------------------------------------------------------------- 1 | // Package types provides the core types for the MCP server SDK. 2 | package types 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | // ClientSession represents an active client connection to the MCP server. 11 | type ClientSession struct { 12 | ID string 13 | UserAgent string 14 | Connected bool 15 | } 16 | 17 | // NewClientSession creates a new ClientSession with a unique ID. 18 | func NewClientSession(userAgent string) *ClientSession { 19 | return &ClientSession{ 20 | ID: uuid.New().String(), 21 | UserAgent: userAgent, 22 | Connected: true, 23 | } 24 | } 25 | 26 | // Resource represents a resource that can be requested by clients. 27 | type Resource struct { 28 | URI string 29 | Name string 30 | Description string 31 | MIMEType string 32 | } 33 | 34 | // ResourceContents represents the contents of a resource. 35 | type ResourceContents struct { 36 | URI string 37 | MIMEType string 38 | Content []byte 39 | Text string 40 | } 41 | 42 | // Tool represents a tool that can be called by clients. 43 | type Tool struct { 44 | Name string 45 | Description string 46 | Parameters []ToolParameter 47 | InputSchema map[string]interface{} `json:"inputSchema,omitempty"` 48 | } 49 | 50 | // ToolParameter defines a parameter for a tool. 51 | type ToolParameter struct { 52 | Name string 53 | Description string 54 | Type string 55 | Required bool 56 | Items map[string]interface{} 57 | } 58 | 59 | // ToolCall represents a request to execute a tool. 60 | type ToolCall struct { 61 | Name string 62 | Parameters map[string]interface{} 63 | Session *ClientSession 64 | } 65 | 66 | // ToolResult represents the result of a tool execution. 67 | type ToolResult struct { 68 | Data interface{} 69 | Error error 70 | } 71 | 72 | // Prompt represents a prompt template that can be rendered. 73 | type Prompt struct { 74 | Name string 75 | Description string 76 | Template string 77 | Parameters []PromptParameter 78 | } 79 | 80 | // PromptParameter defines a parameter for a prompt template. 81 | type PromptParameter struct { 82 | Name string 83 | Description string 84 | Type string 85 | Required bool 86 | } 87 | 88 | // PromptRequest represents a request to render a prompt. 89 | type PromptRequest struct { 90 | Name string 91 | Parameters map[string]interface{} 92 | Session *ClientSession 93 | } 94 | 95 | // PromptResult represents the result of a prompt rendering. 96 | type PromptResult struct { 97 | Text string 98 | Error error 99 | } 100 | 101 | // Notification represents a notification that can be sent to clients. 102 | type Notification struct { 103 | Method string 104 | Params map[string]interface{} 105 | } 106 | 107 | // ResourceRepository defines the interface for managing resources. 108 | type ResourceRepository interface { 109 | // GetResource retrieves a resource by its URI. 110 | GetResource(ctx context.Context, uri string) (*Resource, error) 111 | 112 | // ListResources returns all available resources. 113 | ListResources(ctx context.Context) ([]*Resource, error) 114 | 115 | // AddResource adds a new resource to the repository. 116 | AddResource(ctx context.Context, resource *Resource) error 117 | 118 | // DeleteResource removes a resource from the repository. 119 | DeleteResource(ctx context.Context, uri string) error 120 | } 121 | 122 | // ToolRepository defines the interface for managing tools. 123 | type ToolRepository interface { 124 | // GetTool retrieves a tool by its name. 125 | GetTool(ctx context.Context, name string) (*Tool, error) 126 | 127 | // ListTools returns all available tools. 128 | ListTools(ctx context.Context) ([]*Tool, error) 129 | 130 | // AddTool adds a new tool to the repository. 131 | AddTool(ctx context.Context, tool *Tool) error 132 | 133 | // DeleteTool removes a tool from the repository. 134 | DeleteTool(ctx context.Context, name string) error 135 | } 136 | 137 | // PromptRepository defines the interface for managing prompts. 138 | type PromptRepository interface { 139 | // GetPrompt retrieves a prompt by its name. 140 | GetPrompt(ctx context.Context, name string) (*Prompt, error) 141 | 142 | // ListPrompts returns all available prompts. 143 | ListPrompts(ctx context.Context) ([]*Prompt, error) 144 | 145 | // AddPrompt adds a new prompt to the repository. 146 | AddPrompt(ctx context.Context, prompt *Prompt) error 147 | 148 | // DeletePrompt removes a prompt from the repository. 149 | DeletePrompt(ctx context.Context, name string) error 150 | } 151 | 152 | // SessionRepository defines the interface for managing client sessions. 153 | type SessionRepository interface { 154 | // GetSession retrieves a session by its ID. 155 | GetSession(ctx context.Context, id string) (*ClientSession, error) 156 | 157 | // ListSessions returns all active sessions. 158 | ListSessions(ctx context.Context) ([]*ClientSession, error) 159 | 160 | // AddSession adds a new session to the repository. 161 | AddSession(ctx context.Context, session *ClientSession) error 162 | 163 | // DeleteSession removes a session from the repository. 164 | DeleteSession(ctx context.Context, id string) error 165 | } 166 | 167 | // NotificationSender defines the interface for sending notifications to clients. 168 | type NotificationSender interface { 169 | // SendNotification sends a notification to a specific client. 170 | SendNotification(ctx context.Context, sessionID string, notification *Notification) error 171 | 172 | // BroadcastNotification sends a notification to all connected clients. 173 | BroadcastNotification(ctx context.Context, notification *Notification) error 174 | } 175 | -------------------------------------------------------------------------------- /repository_diagram.md: -------------------------------------------------------------------------------- 1 | ## Repository Diagram 2 | 3 | ```mermaid 4 | flowchart TD 5 | %% Interfaces / Transports 6 | subgraph "Interfaces/Transports" 7 | STDIO["STDIO Interface (internal/interfaces/stdio)"]:::interface 8 | REST["REST-like Interface (internal/interfaces/rest)"]:::interface 9 | end 10 | 11 | %% MCP Server Core 12 | subgraph "MCP Server Core" 13 | SERVER["Server Logic (pkg/server)"]:::core 14 | LIFECYCLE["Lifecycle/Use Cases (internal/usecases/server)"]:::core 15 | INFRA_SERVER["Server Infrastructure (internal/infrastructure/server)"]:::core 16 | end 17 | 18 | %% Domain Models & Business Logic 19 | subgraph "Domain Models & Business Logic" 20 | DOMAIN["Domain Models (internal/domain)"]:::domain 21 | end 22 | 23 | %% Tools & Providers 24 | subgraph "Tools & Providers" 25 | TOOLS["Tools Module (pkg/tools)"]:::tools 26 | PLUGIN["Provider Plugin System (pkg/plugin)"]:::tools 27 | PROVIDER_EX["Provider Examples (examples/providers)"]:::tools 28 | end 29 | 30 | %% Infrastructure & Builders 31 | subgraph "Infrastructure & Builders" 32 | LOGGING["Logging (internal/infrastructure/logging)"]:::infra 33 | INTERNAL_BUILDER["Internal Builder (internal/builder/serverbuilder)"]:::infra 34 | PUBLIC_BUILDER["Public Builder (pkg/builder)"]:::infra 35 | end 36 | 37 | %% Connections from Interfaces to MCP Server Core 38 | STDIO -->|"sends request"| SERVER 39 | REST -->|"sends request"| SERVER 40 | 41 | %% Internal Server Core flow 42 | SERVER -->|"triggers"| LIFECYCLE 43 | LIFECYCLE -->|"utilizes"| INFRA_SERVER 44 | 45 | %% MCP Server Core interactions with other components 46 | SERVER -->|"processes"| DOMAIN 47 | SERVER -->|"invokes"| TOOLS 48 | SERVER -->|"invokes"| PLUGIN 49 | SERVER -->|"logs via"| LOGGING 50 | SERVER -->|"builds using"| INTERNAL_BUILDER 51 | SERVER -->|"builds using"| PUBLIC_BUILDER 52 | 53 | %% Additional connection 54 | INFRA_SERVER -->|"integrates with"| LOGGING 55 | 56 | %% Click Events 57 | click SERVER "https://github.com/freepeak/cortex/blob/main/pkg/server/server.go" 58 | click LIFECYCLE "https://github.com/freepeak/cortex/blob/main/internal/usecases/server.go" 59 | click INFRA_SERVER "https://github.com/freepeak/cortex/tree/main/internal/infrastructure/server/" 60 | click STDIO "https://github.com/freepeak/cortex/tree/main/internal/interfaces/stdio/" 61 | click REST "https://github.com/freepeak/cortex/blob/main/internal/interfaces/rest/server.go" 62 | click DOMAIN "https://github.com/freepeak/cortex/tree/main/internal/domain/" 63 | click TOOLS "https://github.com/freepeak/cortex/blob/main/pkg/tools/helper.go" 64 | click PLUGIN "https://github.com/freepeak/cortex/tree/main/pkg/plugin/" 65 | click PROVIDER_EX "https://github.com/freepeak/cortex/tree/main/examples/providers/" 66 | click LOGGING "https://github.com/freepeak/cortex/tree/main/internal/infrastructure/logging/" 67 | click INTERNAL_BUILDER "https://github.com/freepeak/cortex/blob/main/internal/builder/serverbuilder.go" 68 | click PUBLIC_BUILDER "https://github.com/freepeak/cortex/tree/main/pkg/builder/" 69 | 70 | %% Styles 71 | classDef interface fill:#ffe6f2,stroke:#cc0066,stroke-width:2px; 72 | classDef core fill:#e6f2ff,stroke:#0066cc,stroke-width:2px; 73 | classDef domain fill:#e6ffe6,stroke:#00cc44,stroke-width:2px; 74 | classDef tools fill:#fff0e6,stroke:#ff6600,stroke-width:2px; 75 | classDef infra fill:#ffffe6,stroke:#cccc00,stroke-width:2px; 76 | ``` 77 | -------------------------------------------------------------------------------- /res.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc":"2.0","id":1,"result":{"content":[{"text":"Hello, SSE Server!","type":"text"}]}} 2 | -------------------------------------------------------------------------------- /test-call.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Build the echo server 4 | go build -o bin/echo-stdio-server cmd/echo-stdio-server/main.go 5 | 6 | # Test calling the tool with the platform-prefixed name 7 | echo "Testing platform-prefixed tool 'cortex_echo'..." 8 | echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"cortex_echo","parameters":{"message":"Hello, MCP Server!"}}}' | ./bin/echo-stdio-server 9 | 10 | echo "Test completed!" 11 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | # Golang Codebase Review and Recommendations 2 | 3 | Since I don't have direct access to your codebase, I'll provide general recommendations based on Go best practices and the architectural guidelines in your cursor rules. These suggestions focus on SSE connections, server initialization, and applicable design patterns. 4 | 5 | ## Keeping SSE Connections Alive 6 | 7 | For persistent SSE connections: 8 | 9 | 1. **Context Management** 10 | ```go 11 | func handleSSE(w http.ResponseWriter, r *http.Request) { 12 | // Set headers for SSE 13 | w.Header().Set("Content-Type", "text/event-stream") 14 | w.Header().Set("Cache-Control", "no-cache") 15 | w.Header().Set("Connection", "keep-alive") 16 | 17 | // Create a context that doesn't automatically cancel 18 | ctx, cancel := context.WithCancel(context.Background()) 19 | defer cancel() 20 | 21 | // Handle client disconnection 22 | notify := r.Context().Done() 23 | go func() { 24 | <-notify 25 | cancel() 26 | }() 27 | 28 | // Keep-alive mechanism 29 | ticker := time.NewTicker(30 * time.Second) 30 | defer ticker.Stop() 31 | 32 | flusher, ok := w.(http.Flusher) 33 | if !ok { 34 | http.Error(w, "Streaming not supported", http.StatusInternalServerError) 35 | return 36 | } 37 | 38 | for { 39 | select { 40 | case <-ctx.Done(): 41 | return 42 | case <-ticker.C: 43 | // Send keep-alive message 44 | fmt.Fprintf(w, "event: ping\ndata: %s\n\n", time.Now().String()) 45 | flusher.Flush() 46 | case event := <-eventChannel: 47 | // Send actual event data 48 | fmt.Fprintf(w, "event: message\ndata: %s\n\n", event) 49 | flusher.Flush() 50 | } 51 | } 52 | } 53 | ``` 54 | 55 | 2. **Connection Pool Pattern** 56 | - Implement a registry for active SSE connections 57 | - Allows graceful shutdown and broadcasting to all clients 58 | 59 | ## Simplifying MCP Server Initialization 60 | 61 | Apply these design patterns for easier server initialization: 62 | 63 | 1. **Builder Pattern** 64 | ```go 65 | type MCPServerBuilder struct { 66 | server *MCPServer 67 | } 68 | 69 | func NewMCPServerBuilder() *MCPServerBuilder { 70 | return &MCPServerBuilder{ 71 | server: &MCPServer{ 72 | // Default values 73 | port: 8080, 74 | }, 75 | } 76 | } 77 | 78 | func (b *MCPServerBuilder) WithPort(port int) *MCPServerBuilder { 79 | b.server.port = port 80 | return b 81 | } 82 | 83 | func (b *MCPServerBuilder) WithDatabase(db *Database) *MCPServerBuilder { 84 | b.server.db = db 85 | return b 86 | } 87 | 88 | func (b *MCPServerBuilder) WithLogger(logger Logger) *MCPServerBuilder { 89 | b.server.logger = logger 90 | return b 91 | } 92 | 93 | func (b *MCPServerBuilder) Build() (*MCPServer, error) { 94 | // Validate configurations 95 | if b.server.db == nil { 96 | return nil, errors.New("database is required") 97 | } 98 | 99 | // Additional setup/initialization 100 | return b.server, nil 101 | } 102 | ``` 103 | 104 | 2. **Functional Options Pattern** 105 | ```go 106 | type MCPServerOption func(*MCPServer) 107 | 108 | func WithPort(port int) MCPServerOption { 109 | return func(s *MCPServer) { 110 | s.port = port 111 | } 112 | } 113 | 114 | func WithLogger(logger Logger) MCPServerOption { 115 | return func(s *MCPServer) { 116 | s.logger = logger 117 | } 118 | } 119 | 120 | func NewMCPServer(options ...MCPServerOption) *MCPServer { 121 | server := &MCPServer{ 122 | // Default values 123 | port: 8080, 124 | logger: DefaultLogger{}, 125 | } 126 | 127 | // Apply all options 128 | for _, option := range options { 129 | option(server) 130 | } 131 | 132 | return server 133 | } 134 | 135 | // Usage 136 | // server := NewMCPServer(WithPort(9000), WithLogger(customLogger)) 137 | ``` 138 | 139 | ## General Refactoring Recommendations 140 | 141 | 1. **Dependency Injection** 142 | - Use constructor injection for dependencies 143 | - Define interfaces at the point of use, not implementation 144 | 145 | 2. **Repository Pattern Improvements** 146 | - Abstract database operations behind repository interfaces 147 | - Use factory methods to create repositories 148 | ```go 149 | type UserRepositoryFactory interface { 150 | CreateRepository(ctx context.Context) (UserRepository, error) 151 | } 152 | ``` 153 | 154 | 3. **Use Context Propagation** 155 | - Ensure contexts are properly passed through all layers 156 | - Add timeouts at appropriate boundaries 157 | 158 | 4. **Circuit Breaker Pattern for External Services** 159 | ```go 160 | type CircuitBreaker struct { 161 | failureThreshold int 162 | resetTimeout time.Duration 163 | failures int 164 | lastFailure time.Time 165 | state string // "closed", "open", "half-open" 166 | mu sync.Mutex 167 | } 168 | ``` 169 | 170 | 5. **Command Pattern for Operations** 171 | - Encapsulate operations as command objects 172 | - Enables undo/redo, logging, and queuing 173 | 174 | 6. **Implement Graceful Shutdown** 175 | ```go 176 | func (s *Server) Start() error { 177 | // Server configuration 178 | srv := &http.Server{ 179 | Addr: fmt.Sprintf(":%d", s.port), 180 | Handler: s.router, 181 | } 182 | 183 | // Channel to listen for errors coming from the listener 184 | serverErrors := make(chan error, 1) 185 | 186 | // Start the server 187 | go func() { 188 | serverErrors <- srv.ListenAndServe() 189 | }() 190 | 191 | // Channel for listening to OS signals 192 | osSignals := make(chan os.Signal, 1) 193 | signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM) 194 | 195 | // Block until an OS signal or an error is received 196 | select { 197 | case err := <-serverErrors: 198 | return fmt.Errorf("server error: %w", err) 199 | 200 | case <-osSignals: 201 | // Graceful shutdown 202 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 203 | defer cancel() 204 | 205 | if err := srv.Shutdown(ctx); err != nil { 206 | // If shutdown times out, force close 207 | if err := srv.Close(); err != nil { 208 | return fmt.Errorf("could not stop server: %w", err) 209 | } 210 | return fmt.Errorf("could not gracefully stop server: %w", err) 211 | } 212 | } 213 | 214 | return nil 215 | } 216 | ``` 217 | 218 | For more specific advice, I'd need to see your actual code. Consider using agent mode to allow me to analyze your specific codebase and provide more targeted recommendations. 219 | 220 | -------------------------------------------------------------------------------- /update-imports.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to update import paths from github.com/FreePeak/cortex to github.com/FreePeak/cortex 4 | 5 | # Run from project root 6 | find . -name "*.go" -type f -exec sed -i '' 's|github.com/FreePeak/cortex|github.com/FreePeak/cortex|g' {} \; 7 | 8 | echo "Import paths updated in all Go files" --------------------------------------------------------------------------------