├── .cursorrules ├── .github └── workflows │ ├── go-test.yml │ └── goreleaser.yml ├── .gitignore ├── .goreleaser.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── client.go ├── content_api.go ├── docs ├── .DS_Store ├── README.md ├── architecture.mdx ├── change-notifications.mdx ├── client.mdx ├── contributing.mdx ├── essentials │ ├── code.mdx │ ├── images.mdx │ ├── markdown.mdx │ ├── navigation.mdx │ ├── reusable-snippets.mdx │ └── settings.mdx ├── favicon.svg ├── images │ ├── checks-passed.png │ ├── favicon.svg │ ├── hero-dark.svg │ ├── hero-light.svg │ ├── protocol-diagram.svg │ └── tools-example-usage.png ├── introduction.mdx ├── logo │ ├── dark.svg │ ├── light.svg │ ├── mcp-golang-dark.svg │ └── mcp-golang-light.svg ├── mint.json ├── pagination.mdx ├── quickstart.mdx ├── snippets │ └── snippet-intro.mdx └── tools.mdx ├── examples ├── basic_tool_server │ └── basic_tool_server.go ├── client │ ├── README.md │ ├── main.go │ └── server │ │ └── main.go ├── get_weather_tool_server │ └── get_weather_tool_server.go ├── gin_example │ ├── README.md │ └── main.go ├── http_example │ ├── README.md │ ├── auth_example_client │ │ └── main.go │ ├── client │ │ └── main.go │ └── server │ │ └── main.go ├── pagination_example │ ├── README.md │ └── pagination_example.go ├── readme_server │ └── readme_server.go ├── server │ └── main.go ├── simple_tool_docs │ └── simple_tool_docs.go └── updating_registrations_on_the_fly │ └── updating_registrations_on_the_fly.go ├── go.mod ├── go.sum ├── integration_test.go ├── internal ├── datastructures │ └── generic_sync_map.go ├── protocol │ ├── protocol.go │ ├── protocol_test.go │ └── types.go ├── schema │ ├── README.md │ ├── generate.go │ └── mcp-schema-2024-10-07.json ├── testingutils │ └── mock_transport.go └── tools │ └── tool_types.go ├── prompt_api.go ├── prompt_response_types.go ├── resource_api.go ├── resource_response_types.go ├── resources └── mcp-golang-logo.webp ├── server.go ├── server_test.go ├── tool_api.go ├── tool_response_types.go └── transport ├── http ├── common.go ├── gin.go ├── http.go └── http_client.go ├── sse ├── internal │ └── sse │ │ └── sse.go ├── sse_server.go └── sse_server_test.go ├── stdio ├── internal │ └── stdio │ │ ├── stdio.go │ │ └── stdio_test.go ├── stdio_server.go └── stdio_server_test.go ├── transport.go └── types.go /.cursorrules: -------------------------------------------------------------------------------- 1 | This repository is an implementation of a golang model context protocol sdk. 2 | 3 | Project docs are available in the docs folder. 4 | 5 | Model context prtocol docs are available at https://modelcontextprotocol.io 6 | 7 | Write tests for all new functionality. 8 | 9 | Use go test to run tests after adding new functionality. -------------------------------------------------------------------------------- /.github/workflows/go-test.yml: -------------------------------------------------------------------------------- 1 | name: Go Tests 2 | 3 | on: 4 | pull_request: 5 | branches: [ main, master ] 6 | push: 7 | branches: [ main, master ] 8 | 9 | jobs: 10 | test: 11 | name: Run Go Tests 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v4 19 | with: 20 | go-version: '1.23' # You can adjust this version as needed 21 | cache: false # Disable caching as its super slow 22 | 23 | - name: Install dependencies 24 | run: go mod download 25 | 26 | - name: Run tests 27 | run: go test -v ./... 28 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v4 22 | with: 23 | go-version: '>=1.23.0' 24 | cache: false # Disable caching as its super slow 25 | 26 | - name: Run GoReleaser 27 | uses: goreleaser/goreleaser-action@v6 28 | with: 29 | distribution: goreleaser 30 | version: latest 31 | args: release --clean 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | before: 3 | hooks: 4 | - go mod tidy 5 | 6 | builds: 7 | - skip: true 8 | 9 | changelog: 10 | sort: asc 11 | filters: 12 | exclude: 13 | - '^docs:' 14 | - '^test:' 15 | - '^ci:' 16 | - Merge pull request 17 | - Merge branch 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We'd love to have you contribute to mcp-golang! 4 | 5 | We take contributions seriously and want to make sure that you have a great experience contributing to this project. 6 | As such we're starting with a 2 day response time for all issues and PRs. If you don't get a response within 2 days, feel free to email me directly at chris -at- metoro (dot) io. 7 | 8 | Please read the following guidelines to get started. 9 | 10 | ## Getting Started 11 | 12 | Before you write a PR, please an open an issue to discuss the changes you'd like to make. This will help us understand what you're trying to achieve and make sure that we're on the same page and that your work will be accepted. 13 | 14 | This doesn't need to be an essay, just a short description of what you're trying to achieve and why then we'll quickly flesh out the details together. 15 | 16 | ## Pull Request Process 17 | 18 | 1. Fork the repository 19 | 2. Create a new branch with a descriptive name 20 | 3. Make your changes 21 | 4. Open the PR and link the issue that you opened in the previous step -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Metoro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Statusphere logo 3 |
4 |
5 |
6 | 7 | ![GitHub stars](https://img.shields.io/github/stars/metoro-io/mcp-golang?style=social) 8 | ![GitHub forks](https://img.shields.io/github/forks/metoro-io/mcp-golang?style=social) 9 | ![GitHub issues](https://img.shields.io/github/issues/metoro-io/mcp-golang) 10 | ![GitHub pull requests](https://img.shields.io/github/issues-pr/metoro-io/mcp-golang) 11 | ![GitHub license](https://img.shields.io/github/license/metoro-io/mcp-golang) 12 | ![GitHub contributors](https://img.shields.io/github/contributors/metoro-io/mcp-golang) 13 | ![GitHub last commit](https://img.shields.io/github/last-commit/metoro-io/mcp-golang) 14 | [![GoDoc](https://pkg.go.dev/badge/github.com/metoro-io/mcp-golang.svg)](https://pkg.go.dev/github.com/metoro-io/mcp-golang) 15 | [![Go Report Card](https://goreportcard.com/badge/github.com/metoro-io/mcp-golang)](https://goreportcard.com/report/github.com/metoro-io/mcp-golang) 16 | ![Tests](https://github.com/metoro-io/mcp-golang/actions/workflows/go-test.yml/badge.svg) 17 | 18 | 19 | 20 | 21 |
22 | 23 | # mcp-golang 24 | 25 | mcp-golang is an unofficial implementation of the [Model Context Protocol](https://modelcontextprotocol.io/) in Go. 26 | 27 | Write MCP servers and clients in golang with a few lines of code. 28 | 29 | Docs at [https://mcpgolang.com](https://mcpgolang.com) 30 | 31 | ## Highlights 32 | - 🛡️**Type safety** - Define your tool arguments as native go structs, have mcp-golang handle the rest. Automatic schema generation, deserialization, error handling etc. 33 | - 🚛 **Custom transports** - Use the built-in transports (stdio for full feature support, HTTP for stateless communication) or write your own. 34 | - ⚡ **Low boilerplate** - mcp-golang generates all the MCP endpoints for you apart from your tools, prompts and resources. 35 | - 🧩 **Modular** - The library is split into three components: transport, protocol and server/client. Use them all or take what you need. 36 | - 🔄 **Bi-directional** - Full support for both server and client implementations through stdio transport. 37 | 38 | ## Example Usage 39 | 40 | Install with `go get github.com/metoro-io/mcp-golang` 41 | 42 | ### Server Example 43 | 44 | ```go 45 | package main 46 | 47 | import ( 48 | "fmt" 49 | "github.com/metoro-io/mcp-golang" 50 | "github.com/metoro-io/mcp-golang/transport/stdio" 51 | ) 52 | 53 | // Tool arguments are just structs, annotated with jsonschema tags 54 | // More at https://mcpgolang.com/tools#schema-generation 55 | type Content struct { 56 | Title string `json:"title" jsonschema:"required,description=The title to submit"` 57 | Description *string `json:"description" jsonschema:"description=The description to submit"` 58 | } 59 | type MyFunctionsArguments struct { 60 | Submitter string `json:"submitter" jsonschema:"required,description=The name of the thing calling this tool (openai, google, claude, etc)"` 61 | Content Content `json:"content" jsonschema:"required,description=The content of the message"` 62 | } 63 | 64 | func main() { 65 | done := make(chan struct{}) 66 | 67 | server := mcp_golang.NewServer(stdio.NewStdioServerTransport()) 68 | err := server.RegisterTool("hello", "Say hello to a person", func(arguments MyFunctionsArguments) (*mcp_golang.ToolResponse, error) { 69 | return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Hello, %server!", arguments.Submitter))), nil 70 | }) 71 | if err != nil { 72 | panic(err) 73 | } 74 | 75 | err = server.RegisterPrompt("promt_test", "This is a test prompt", func(arguments Content) (*mcp_golang.PromptResponse, error) { 76 | return mcp_golang.NewPromptResponse("description", mcp_golang.NewPromptMessage(mcp_golang.NewTextContent(fmt.Sprintf("Hello, %server!", arguments.Title)), mcp_golang.RoleUser)), nil 77 | }) 78 | if err != nil { 79 | panic(err) 80 | } 81 | 82 | err = server.RegisterResource("test://resource", "resource_test", "This is a test resource", "application/json", func() (*mcp_golang.ResourceResponse, error) { 83 | return mcp_golang.NewResourceResponse(mcp_golang.NewTextEmbeddedResource("test://resource", "This is a test resource", "application/json")), nil 84 | }) 85 | 86 | err = server.Serve() 87 | if err != nil { 88 | panic(err) 89 | } 90 | 91 | <-done 92 | } 93 | ``` 94 | 95 | ### HTTP Server Example 96 | 97 | You can also create an HTTP-based server using either the standard HTTP transport or Gin framework: 98 | 99 | ```go 100 | // Standard HTTP 101 | transport := http.NewHTTPTransport("/mcp") 102 | transport.WithAddr(":8080") 103 | server := mcp_golang.NewServer(transport) 104 | 105 | // Or with Gin framework 106 | transport := http.NewGinTransport() 107 | router := gin.Default() 108 | router.POST("/mcp", transport.Handler()) 109 | server := mcp_golang.NewServer(transport) 110 | ``` 111 | 112 | Note: HTTP transports are stateless and don't support bidirectional features like notifications. Use stdio transport if you need those features. 113 | 114 | ### Client Example 115 | 116 | Checkout the [examples/client](./examples/client) directory for a more complete example. 117 | 118 | ```go 119 | package main 120 | 121 | import ( 122 | "context" 123 | "log" 124 | mcp "github.com/metoro-io/mcp-golang" 125 | "github.com/metoro-io/mcp-golang/transport/stdio" 126 | ) 127 | 128 | // Define type-safe arguments 129 | type CalculateArgs struct { 130 | Operation string `json:"operation"` 131 | A int `json:"a"` 132 | B int `json:"b"` 133 | } 134 | 135 | func main() { 136 | cmd := exec.Command("go", "run", "./server/main.go") 137 | stdin, err := cmd.StdinPipe() 138 | if err != nil { 139 | log.Fatalf("Failed to get stdin pipe: %v", err) 140 | } 141 | stdout, err := cmd.StdoutPipe() 142 | if err != nil { 143 | log.Fatalf("Failed to get stdout pipe: %v", err) 144 | } 145 | 146 | if err := cmd.Start(); err != nil { 147 | log.Fatalf("Failed to start server: %v", err) 148 | } 149 | defer cmd.Process.Kill() 150 | // Create and initialize client 151 | transport := stdio.NewStdioServerTransportWithIO(stdout, stdin) 152 | client := mcp.NewClient(transport) 153 | 154 | if _, err := client.Initialize(context.Background()); err != nil { 155 | log.Fatalf("Failed to initialize: %v", err) 156 | } 157 | 158 | // Call a tool with typed arguments 159 | args := CalculateArgs{ 160 | Operation: "add", 161 | A: 10, 162 | B: 5, 163 | } 164 | 165 | response, err := client.CallTool(context.Background(), "calculate", args) 166 | if err != nil { 167 | log.Fatalf("Failed to call tool: %v", err) 168 | } 169 | 170 | if response != nil && len(response.Content) > 0 { 171 | log.Printf("Result: %s", response.Content[0].TextContent.Text) 172 | } 173 | } 174 | ``` 175 | 176 | ### Using with Claude Desktop 177 | 178 | Create a file in ~/Library/Application Support/Claude/claude_desktop_config.json with the following contents: 179 | 180 | ```json 181 | { 182 | "mcpServers": { 183 | "golang-mcp-server": { 184 | "command": "", 185 | "args": [], 186 | "env": {} 187 | } 188 | } 189 | } 190 | ``` 191 | 192 | ## Contributions 193 | 194 | Contributions are more than welcome! Please check out [our contribution guidelines](./CONTRIBUTING.md). 195 | 196 | ## Discord 197 | 198 | Got any suggestions, have a question on the api or usage? Ask on the [discord server](https://discord.gg/33saRwE3pT). 199 | A maintainer will be happy to help you out. 200 | 201 | ## Examples 202 | 203 | Some more extensive examples using the library found here: 204 | 205 | - **[Metoro](https://github.com/metoro-io/metoro-mcp-server)** - Query and interact with kubernetes environments monitored by Metoro 206 | 207 | Open a PR to add your own projects! 208 | 209 | ## Server Feature Implementation 210 | 211 | ### Tools 212 | - [x] Tool Calls 213 | - [x] Native go structs as arguments 214 | - [x] Programatically generated tool list endpoint 215 | - [x] Change notifications 216 | - [x] Pagination 217 | 218 | ### Prompts 219 | - [x] Prompt Calls 220 | - [x] Programatically generated prompt list endpoint 221 | - [x] Change notifications 222 | - [x] Pagination 223 | 224 | ### Resources 225 | - [x] Resource Calls 226 | - [x] Programatically generated resource list endpoint 227 | - [x] Change notifications 228 | - [x] Pagination 229 | 230 | ### Transports 231 | - [x] Stdio - Full support for all features including bidirectional communication 232 | - [x] HTTP - Stateless transport for simple request-response scenarios (no notifications support) 233 | - [x] Gin - HTTP transport with Gin framework integration (stateless, no notifications support) 234 | - [x] SSE 235 | - [x] Custom transport support 236 | - [ ] HTTPS with custom auth support - in progress. Not currently part of the spec but we'll be adding experimental support for it. 237 | 238 | ### Client 239 | - [x] Call tools 240 | - [x] Call prompts 241 | - [x] Call resources 242 | - [x] List tools 243 | - [x] List prompts 244 | - [x] List resources 245 | 246 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package mcp_golang 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/metoro-io/mcp-golang/internal/protocol" 8 | "github.com/metoro-io/mcp-golang/transport" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // Client represents an MCP client that can connect to and interact with MCP servers 13 | type Client struct { 14 | transport transport.Transport 15 | protocol *protocol.Protocol 16 | capabilities *ServerCapabilities 17 | initialized bool 18 | info ClientInfo 19 | } 20 | 21 | // NewClient creates a new MCP client with the specified transport 22 | func NewClient(transport transport.Transport) *Client { 23 | return &Client{ 24 | transport: transport, 25 | protocol: protocol.NewProtocol(nil), 26 | } 27 | } 28 | 29 | type ClientInfo struct { 30 | Name string `json:"name"` 31 | Version string `json:"version"` 32 | } 33 | 34 | // NewClientWithInfo create a new client with info. This is required by anthorpic mcp tools 35 | func NewClientWithInfo(transport transport.Transport, info ClientInfo) *Client { 36 | return &Client{ 37 | transport: transport, 38 | protocol: protocol.NewProtocol(nil), 39 | info: info, 40 | } 41 | } 42 | 43 | // Initialize connects to the server and retrieves its capabilities 44 | func (c *Client) Initialize(ctx context.Context) (*InitializeResponse, error) { 45 | if c.initialized { 46 | return nil, errors.New("client already initialized") 47 | } 48 | 49 | err := c.protocol.Connect(c.transport) 50 | if err != nil { 51 | return nil, errors.Wrap(err, "failed to connect transport") 52 | } 53 | 54 | // Make initialize request to server 55 | response, err := c.protocol.Request(ctx, "initialize", map[string]interface{}{ 56 | "protocolVersion": "1.0", 57 | "capabilities": map[string]interface{}{}, 58 | "clientInfo": c.info, 59 | }, nil) 60 | if err != nil { 61 | return nil, errors.Wrap(err, "failed to initialize") 62 | } 63 | 64 | responseBytes, ok := response.(json.RawMessage) 65 | if !ok { 66 | return nil, errors.New("invalid response type") 67 | } 68 | 69 | var initResult InitializeResponse 70 | err = json.Unmarshal(responseBytes, &initResult) 71 | if err != nil { 72 | return nil, errors.Wrap(err, "failed to unmarshal initialize response") 73 | } 74 | 75 | c.capabilities = &initResult.Capabilities 76 | c.initialized = true 77 | return &initResult, nil 78 | } 79 | 80 | // ListTools retrieves the list of available tools from the server 81 | func (c *Client) ListTools(ctx context.Context, cursor *string) (*ToolsResponse, error) { 82 | if !c.initialized { 83 | return nil, errors.New("client not initialized") 84 | } 85 | 86 | params := map[string]interface{}{ 87 | "cursor": cursor, 88 | } 89 | 90 | response, err := c.protocol.Request(ctx, "tools/list", params, nil) 91 | if err != nil { 92 | return nil, errors.Wrap(err, "failed to list tools") 93 | } 94 | 95 | responseBytes, ok := response.(json.RawMessage) 96 | if !ok { 97 | return nil, errors.New("invalid response type") 98 | } 99 | 100 | var toolsResponse ToolsResponse 101 | err = json.Unmarshal(responseBytes, &toolsResponse) 102 | if err != nil { 103 | return nil, errors.Wrap(err, "failed to unmarshal tools response") 104 | } 105 | 106 | return &toolsResponse, nil 107 | } 108 | 109 | // CallTool calls a specific tool on the server with the provided arguments 110 | func (c *Client) CallTool(ctx context.Context, name string, arguments any) (*ToolResponse, error) { 111 | if !c.initialized { 112 | return nil, errors.New("client not initialized") 113 | } 114 | 115 | argumentsJson, err := json.Marshal(arguments) 116 | if err != nil { 117 | return nil, errors.Wrap(err, "failed to marshal arguments") 118 | } 119 | 120 | params := baseCallToolRequestParams{ 121 | Name: name, 122 | Arguments: argumentsJson, 123 | } 124 | 125 | response, err := c.protocol.Request(ctx, "tools/call", params, nil) 126 | if err != nil { 127 | return nil, errors.Wrap(err, "failed to call tool") 128 | } 129 | 130 | responseBytes, ok := response.(json.RawMessage) 131 | if !ok { 132 | return nil, errors.New("invalid response type") 133 | } 134 | 135 | var toolResponse ToolResponse 136 | err = json.Unmarshal(responseBytes, &toolResponse) 137 | if err != nil { 138 | return nil, errors.Wrap(err, "failed to unmarshal tool response") 139 | } 140 | 141 | return &toolResponse, nil 142 | } 143 | 144 | // ListPrompts retrieves the list of available prompts from the server 145 | func (c *Client) ListPrompts(ctx context.Context, cursor *string) (*ListPromptsResponse, error) { 146 | if !c.initialized { 147 | return nil, errors.New("client not initialized") 148 | } 149 | 150 | params := map[string]interface{}{ 151 | "cursor": cursor, 152 | } 153 | 154 | response, err := c.protocol.Request(ctx, "prompts/list", params, nil) 155 | if err != nil { 156 | return nil, errors.Wrap(err, "failed to list prompts") 157 | } 158 | 159 | responseBytes, ok := response.(json.RawMessage) 160 | if !ok { 161 | return nil, errors.New("invalid response type") 162 | } 163 | 164 | var promptsResponse ListPromptsResponse 165 | err = json.Unmarshal(responseBytes, &promptsResponse) 166 | if err != nil { 167 | return nil, errors.Wrap(err, "failed to unmarshal prompts response") 168 | } 169 | 170 | return &promptsResponse, nil 171 | } 172 | 173 | // GetPrompt retrieves a specific prompt from the server 174 | func (c *Client) GetPrompt(ctx context.Context, name string, arguments any) (*PromptResponse, error) { 175 | if !c.initialized { 176 | return nil, errors.New("client not initialized") 177 | } 178 | 179 | argumentsJson, err := json.Marshal(arguments) 180 | if err != nil { 181 | return nil, errors.Wrap(err, "failed to marshal arguments") 182 | } 183 | 184 | params := baseGetPromptRequestParamsArguments{ 185 | Name: name, 186 | Arguments: argumentsJson, 187 | } 188 | 189 | response, err := c.protocol.Request(ctx, "prompts/get", params, nil) 190 | if err != nil { 191 | return nil, errors.Wrap(err, "failed to get prompt") 192 | } 193 | 194 | responseBytes, ok := response.(json.RawMessage) 195 | if !ok { 196 | return nil, errors.New("invalid response type") 197 | } 198 | 199 | var promptResponse PromptResponse 200 | err = json.Unmarshal(responseBytes, &promptResponse) 201 | if err != nil { 202 | return nil, errors.Wrap(err, "failed to unmarshal prompt response") 203 | } 204 | 205 | return &promptResponse, nil 206 | } 207 | 208 | // ListResources retrieves the list of available resources from the server 209 | func (c *Client) ListResources(ctx context.Context, cursor *string) (*ListResourcesResponse, error) { 210 | if !c.initialized { 211 | return nil, errors.New("client not initialized") 212 | } 213 | 214 | params := map[string]interface{}{ 215 | "cursor": cursor, 216 | } 217 | 218 | response, err := c.protocol.Request(ctx, "resources/list", params, nil) 219 | if err != nil { 220 | return nil, errors.Wrap(err, "failed to list resources") 221 | } 222 | 223 | responseBytes, ok := response.(json.RawMessage) 224 | if !ok { 225 | return nil, errors.New("invalid response type") 226 | } 227 | 228 | var resourcesResponse ListResourcesResponse 229 | err = json.Unmarshal(responseBytes, &resourcesResponse) 230 | if err != nil { 231 | return nil, errors.Wrap(err, "failed to unmarshal resources response") 232 | } 233 | 234 | return &resourcesResponse, nil 235 | } 236 | 237 | // ReadResource reads a specific resource from the server 238 | func (c *Client) ReadResource(ctx context.Context, uri string) (*ResourceResponse, error) { 239 | if !c.initialized { 240 | return nil, errors.New("client not initialized") 241 | } 242 | 243 | params := readResourceRequestParams{ 244 | Uri: uri, 245 | } 246 | 247 | response, err := c.protocol.Request(ctx, "resources/read", params, nil) 248 | if err != nil { 249 | return nil, errors.Wrap(err, "failed to read resource") 250 | } 251 | 252 | responseBytes, ok := response.(json.RawMessage) 253 | if !ok { 254 | return nil, errors.New("invalid response type") 255 | } 256 | 257 | var resourceResponse ResourceResponse 258 | err = json.Unmarshal(responseBytes, &resourceResponse) 259 | if err != nil { 260 | return nil, errors.Wrap(err, "failed to unmarshal resource response") 261 | } 262 | 263 | return &resourceResponse, nil 264 | } 265 | 266 | // Ping sends a ping request to the server to check connectivity 267 | func (c *Client) Ping(ctx context.Context) error { 268 | if !c.initialized { 269 | return errors.New("client not initialized") 270 | } 271 | 272 | _, err := c.protocol.Request(ctx, "ping", nil, nil) 273 | if err != nil { 274 | return errors.Wrap(err, "failed to ping server") 275 | } 276 | 277 | return nil 278 | } 279 | 280 | // GetCapabilities returns the server capabilities obtained during initialization 281 | func (c *Client) GetCapabilities() *ServerCapabilities { 282 | return c.capabilities 283 | } 284 | -------------------------------------------------------------------------------- /docs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metoro-io/mcp-golang/6598b3d737ebf5246e30df49aa2d0daf7f04fd17/docs/.DS_Store -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Mintlify Starter Kit 2 | 3 | Click on `Use this template` to copy the Mintlify starter kit. The starter kit contains examples including 4 | 5 | - Guide pages 6 | - Navigation 7 | - Customizations 8 | - API Reference pages 9 | - Use of popular components 10 | 11 | ### Development 12 | 13 | Install the [Mintlify CLI](https://www.npmjs.com/package/mintlify) to preview the documentation changes locally. To install, use the following command 14 | 15 | ``` 16 | npm i -g mintlify 17 | ``` 18 | 19 | Run the following command at the root of your documentation (where mint.json is) 20 | 21 | ``` 22 | mintlify dev 23 | ``` 24 | 25 | ### Publishing Changes 26 | 27 | Install our Github App to auto propagate changes from your repo to your deployment. Changes will be deployed to production automatically after pushing to the default branch. Find the link to install on your dashboard. 28 | 29 | #### Troubleshooting 30 | 31 | - Mintlify dev isn't running - Run `mintlify install` it'll re-install dependencies. 32 | - Page loads as a 404 - Make sure you are running in a folder with `mint.json` 33 | -------------------------------------------------------------------------------- /docs/architecture.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Architecture 3 | description: 'mcp-golang library architecture' 4 | --- 5 | 6 | The library is split functionally into three distinct components which mimic the architecture of the MCP protocol itself: 7 | - **The transport layer** - which handles the actual communication between the client. This could be a TCP connection, an HTTP request, or a WebSocket connection or anything else. 8 | It is reponsible for taking those underlying messages and converting them to JSON-RPC messages and taking JSON-RPC messages and converting them to underlying messages. 9 | - **The protocol layer** - which defines the actual protocol for the transport layer, it takes JSON-RPC messages and turns them into requests, notifications, and responses. 10 | It also has a list of handlers which handle requests and notifications, it performs the JSON-RPC method routing and error handling. 11 | - **The server layer** - which is the actual implementation of the server, it takes the protocol layer and the transport layer and builds a server that can handle requests from clients. 12 | It offers a much higher level API to the user where they can register basic handlers for tools, resources, prompts, completions, etc. 13 | - **The user code** itself, which are handlers. These handlers implement the business logic for the tools, resources, prompts, completions, etc and are passed to the server layer. 14 | 15 | MCP Golang architecture diagram -------------------------------------------------------------------------------- /docs/change-notifications.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Change Notifications 3 | description: 'How change notifications are handled in mcp-golang' 4 | --- 5 | 6 | The Model Context Protocol allows clients to get notified when a resource has changed. 7 | This allows clients to do things like refresh the LLMs context when an underlying resource has changed. 8 | Lets say you have some application logs in a resource, the server could periodically refresh the application log resource to allow the client to get latest logs. 9 | 10 | ## Implementation 11 | 12 | In mcp-golang, the server will send a notification to the client when any of the following events occur: 13 | - A new tool is registered via the `RegisterTool` function 14 | - A new prompt is registered via the `RegisterPrompt` function 15 | - A new resource is registered via the `RegisterResource` function 16 | - A prompt is deregistered via the `DeregisterPrompt` function 17 | - A tool is deregistered via the `DeregisterTool` function 18 | - A resource is deregistered via the `DeregisterResource` function 19 | 20 | A silly e2e example of this is the server below. It registers and deregisters a tool, prompt, and resource every second causing 3 notifications to be sent to the client each second. 21 | 22 | ```go 23 | package main 24 | 25 | import ( 26 | "fmt" 27 | mcp_golang "github.com/metoro-io/mcp-golang" 28 | "github.com/metoro-io/mcp-golang/transport/stdio" 29 | "time" 30 | ) 31 | 32 | type HelloArguments struct { 33 | Submitter string `json:"submitter" jsonschema:"required,description=The name of the thing calling this tool (openai or google or claude etc)'"` 34 | } 35 | 36 | type Content struct { 37 | Title string `json:"title" jsonschema:"required,description=The title to submit"` 38 | Description *string `json:"description" jsonschema:"description=The description to submit"` 39 | } 40 | 41 | func main() { 42 | done := make(chan struct{}) 43 | server := mcp_golang.NewServer(stdio.NewStdioServerTransport()) 44 | err := server.Serve() 45 | if err != nil { 46 | panic(err) 47 | } 48 | go func() { 49 | for { 50 | err := server.RegisterTool("hello", "Say hello to a person", func(arguments HelloArguments) (*mcp_golang.ToolResponse, error) { 51 | return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Hello, %s!", arguments.Submitter))), nil 52 | }) 53 | if err != nil { 54 | panic(err) 55 | } 56 | time.Sleep(1 * time.Second) 57 | err = server.DeregisterTool("hello") 58 | if err != nil { 59 | panic(err) 60 | } 61 | } 62 | }() 63 | go func() { 64 | for { 65 | 66 | err = server.RegisterPrompt("prompt_test", "This is a test prompt", func(arguments Content) (*mcp_golang.PromptResponse, error) { 67 | return mcp_golang.NewPromptResponse("description", mcp_golang.NewPromptMessage(mcp_golang.NewTextContent(fmt.Sprintf("Hello, %server!", arguments.Title)), mcp_golang.RoleUser)), nil 68 | }) 69 | if err != nil { 70 | panic(err) 71 | } 72 | time.Sleep(1 * time.Second) 73 | err = server.DeregisterPrompt("prompt_test") 74 | if err != nil { 75 | panic(err) 76 | } 77 | } 78 | 79 | }() 80 | go func() { 81 | err = server.RegisterResource("test://resource", "resource_test", "This is a test resource", "application/json", func() (*mcp_golang.ResourceResponse, error) { 82 | return mcp_golang.NewResourceResponse(mcp_golang.NewTextEmbeddedResource("test://resource", "This is a test resource", "application/json")), nil 83 | }) 84 | if err != nil { 85 | panic(err) 86 | } 87 | time.Sleep(1 * time.Second) 88 | err = server.DeregisterResource("test://resource") 89 | if err != nil { 90 | panic(err) 91 | } 92 | }() 93 | 94 | <-done 95 | } 96 | ``` -------------------------------------------------------------------------------- /docs/client.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Using the MCP Client' 3 | description: 'Learn how to use the MCP client to interact with MCP servers' 4 | --- 5 | 6 | # MCP Client Usage Guide 7 | 8 | The MCP client provides a simple and intuitive way to interact with MCP servers. This guide will walk you through initializing the client, connecting to a server, and using various MCP features. 9 | 10 | ## Installation 11 | 12 | Add the MCP Golang package to your project: 13 | 14 | ```bash 15 | go get github.com/metoro-io/mcp-golang 16 | ``` 17 | 18 | ## Basic Usage 19 | 20 | Here's a simple example of creating and initializing an MCP client: 21 | 22 | ```go 23 | import ( 24 | mcp "github.com/metoro-io/mcp-golang" 25 | "github.com/metoro-io/mcp-golang/transport/stdio" 26 | ) 27 | 28 | // Create a transport (stdio in this example) 29 | transport := stdio.NewStdioServerTransportWithIO(stdout, stdin) 30 | 31 | // Create a new client 32 | client := mcp.NewClient(transport) 33 | 34 | // Initialize the client 35 | response, err := client.Initialize(context.Background()) 36 | if err != nil { 37 | log.Fatalf("Failed to initialize client: %v", err) 38 | } 39 | ``` 40 | 41 | ## Working with Tools 42 | 43 | ### Listing Available Tools 44 | 45 | ```go 46 | tools, err := client.ListTools(context.Background(), nil) 47 | if err != nil { 48 | log.Fatalf("Failed to list tools: %v", err) 49 | } 50 | 51 | for _, tool := range tools.Tools { 52 | log.Printf("Tool: %s. Description: %s", tool.Name, *tool.Description) 53 | } 54 | ``` 55 | 56 | ### Calling a Tool 57 | 58 | ```go 59 | // Define a type-safe struct for your tool arguments 60 | type CalculateArgs struct { 61 | Operation string `json:"operation"` 62 | A int `json:"a"` 63 | B int `json:"b"` 64 | } 65 | 66 | // Create typed arguments 67 | args := CalculateArgs{ 68 | Operation: "add", 69 | A: 10, 70 | B: 5, 71 | } 72 | 73 | response, err := client.CallTool(context.Background(), "calculate", args) 74 | if err != nil { 75 | log.Printf("Failed to call tool: %v", err) 76 | } 77 | 78 | // Handle the response 79 | if response != nil && len(response.Content) > 0 { 80 | if response.Content[0].TextContent != nil { 81 | log.Printf("Response: %s", response.Content[0].TextContent.Text) 82 | } 83 | } 84 | ``` 85 | 86 | ## Working with Prompts 87 | 88 | ### Listing Available Prompts 89 | 90 | ```go 91 | prompts, err := client.ListPrompts(context.Background(), nil) 92 | if err != nil { 93 | log.Printf("Failed to list prompts: %v", err) 94 | } 95 | 96 | for _, prompt := range prompts.Prompts { 97 | log.Printf("Prompt: %s. Description: %s", prompt.Name, *prompt.Description) 98 | } 99 | ``` 100 | 101 | ### Using a Prompt 102 | 103 | ```go 104 | // Define a type-safe struct for your prompt arguments 105 | type PromptArgs struct { 106 | Input string `json:"input"` 107 | } 108 | 109 | // Create typed arguments 110 | args := PromptArgs{ 111 | Input: "Hello, MCP!", 112 | } 113 | 114 | response, err := client.GetPrompt(context.Background(), "prompt_name", args) 115 | if err != nil { 116 | log.Printf("Failed to get prompt: %v", err) 117 | } 118 | 119 | if response != nil && len(response.Messages) > 0 { 120 | log.Printf("Response: %s", response.Messages[0].Content.TextContent.Text) 121 | } 122 | ``` 123 | 124 | ## Working with Resources 125 | 126 | ### Listing Resources 127 | 128 | ```go 129 | resources, err := client.ListResources(context.Background(), nil) 130 | if err != nil { 131 | log.Printf("Failed to list resources: %v", err) 132 | } 133 | 134 | for _, resource := range resources.Resources { 135 | log.Printf("Resource: %s", resource.Uri) 136 | } 137 | ``` 138 | 139 | ### Reading a Resource 140 | 141 | ```go 142 | resource, err := client.ReadResource(context.Background(), "resource_uri") 143 | if err != nil { 144 | log.Printf("Failed to read resource: %v", err) 145 | } 146 | 147 | if resource != nil { 148 | log.Printf("Resource content: %s", resource.Content) 149 | } 150 | ``` 151 | 152 | ## Pagination 153 | 154 | Both `ListTools` and `ListPrompts` support pagination. You can pass a cursor to get the next page of results: 155 | 156 | ```go 157 | var cursor *string 158 | for { 159 | tools, err := client.ListTools(context.Background(), cursor) 160 | if err != nil { 161 | log.Fatalf("Failed to list tools: %v", err) 162 | } 163 | 164 | // Process tools... 165 | 166 | if tools.NextCursor == nil { 167 | break // No more pages 168 | } 169 | cursor = tools.NextCursor 170 | } 171 | ``` 172 | 173 | ## Error Handling 174 | 175 | The client includes comprehensive error handling. All methods return an error as their second return value: 176 | 177 | ```go 178 | response, err := client.CallTool(context.Background(), "calculate", args) 179 | if err != nil { 180 | switch { 181 | case errors.Is(err, mcp.ErrClientNotInitialized): 182 | // Handle initialization error 183 | default: 184 | // Handle other errors 185 | } 186 | } 187 | ``` 188 | 189 | ## Best Practices 190 | 191 | 1. Always initialize the client before making any calls 192 | 2. Use appropriate context management for timeouts and cancellation 193 | 3. Handle errors appropriately for your use case 194 | 4. Close or clean up resources when done 195 | 5. Define type-safe structs for tool and prompt arguments 196 | 6. Use struct tags to ensure correct JSON field names 197 | 198 | ## Complete Example 199 | 200 | For a complete working example, check out our [example client implementation](https://github.com/metoro-io/mcp-golang/tree/main/examples/client). 201 | 202 | ## Transport Options 203 | 204 | The MCP client supports multiple transport options: 205 | 206 | ### Standard I/O Transport 207 | 208 | For command-line tools that communicate through stdin/stdout: 209 | 210 | ```go 211 | transport := stdio.NewStdioClientTransport() 212 | client := mcp.NewClient(transport) 213 | ``` 214 | 215 | This transport supports all MCP features including bidirectional communication and notifications. 216 | 217 | ### HTTP Transport 218 | 219 | For web-based tools that communicate over HTTP/HTTPS: 220 | 221 | ```go 222 | transport := http.NewHTTPClientTransport("/mcp") 223 | transport.WithBaseURL("http://localhost:8080") 224 | client := mcp.NewClient(transport) 225 | ``` 226 | 227 | Note that the HTTP transport is stateless and does not support bidirectional features like notifications. Each request-response cycle is independent, making it suitable for simple tool invocations but not for scenarios requiring real-time updates or persistent connections. 228 | 229 | ## Context Support 230 | 231 | All client operations now support context propagation: 232 | 233 | ```go 234 | ctx := context.Background() 235 | // With timeout 236 | ctx, cancel := context.WithTimeout(ctx, 5*time.Second) 237 | defer cancel() 238 | 239 | // Call tool with context 240 | response, err := client.CallTool(ctx, "tool-name", args) 241 | if err != nil { 242 | // Handle error 243 | } 244 | 245 | // List tools with context 246 | tools, err := client.ListTools(ctx) 247 | if err != nil { 248 | // Handle error 249 | } 250 | ``` 251 | 252 | The context allows you to: 253 | - Set timeouts for operations 254 | - Cancel long-running operations 255 | - Pass request-scoped values 256 | - Implement tracing and monitoring -------------------------------------------------------------------------------- /docs/contributing.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Contributing' 3 | description: 'Guidelines for contributing to the mcp-golang project' 4 | --- 5 | ## Development Guide 6 | 7 | This document provides a step-by-step guide for contributing to the mcp-golang project. By no means is it complete, but it should help you get started. 8 | 9 | ### Development Setup 10 | 11 | To set up your development environment, follow these steps: 12 | 13 | #### Prerequisites 14 | 15 | - Go 1.19 or higher 16 | - Git 17 | 18 | #### Local Development 19 | 20 | 1. Clone the repository: 21 | ```bash 22 | git clone https://github.com/metoro-io/mcp-golang.git 23 | cd mcp-golang 24 | ``` 25 | 26 | 2. Install dependencies: 27 | ```bash 28 | go mod download 29 | ``` 30 | 31 | 3. Run tests: 32 | ```bash 33 | go test ./... 34 | ``` 35 | 36 | ### Project Structure 37 | 38 | The project is organized into several key packages: 39 | 40 | - `server/`: Core server implementation 41 | - `transport/`: Transport layer implementations (stdio, SSE) 42 | - `protocol/`: MCP protocol implementation 43 | - `examples/`: Example implementations 44 | - `internal/`: Internal utilities and helpers 45 | 46 | ### Implementation Guidelines 47 | 48 | #### Creating a Custom Transport 49 | 50 | To implement a custom transport, create a struct that implements the `Transport` interface. 51 | If your transport is not part of the spec then you can add it as an experimental feature. 52 | Before you implement the transport, you should have a good understanding of the MCP protocol. Take a look at https://spec.modelcontextprotocol.io/specification/ 53 | 54 | ### Testing 55 | 56 | #### Unit Tests 57 | 58 | All new functions should have unit tests where possible. We currently use testify for this. 59 | Each test should explain its purpose and expected behavior. E.g. 60 | 61 | ```go 62 | 63 | // TestProtocol_Request tests the core request-response functionality of the protocol. 64 | // This is the most important test as it covers the primary use case of the protocol. 65 | // It includes subtests for: 66 | // 1. Successful request/response with proper correlation 67 | // 2. Request timeout handling 68 | // 3. Request cancellation via context 69 | // These scenarios ensure the protocol can handle both successful and error cases 70 | // while maintaining proper message correlation and resource cleanup. 71 | func TestProtocol_Request(t *testing.T) { 72 | p := NewProtocol(nil) 73 | transport := mcp.newMockTransport() 74 | 75 | if err := p.Connect(transport); err != nil { 76 | t.Fatalf("Connect failed: %v", err) 77 | } 78 | 79 | // Test successful request 80 | t.Run("Successful request", func(t *testing.T) { 81 | ctx := context.Background() 82 | go func() { 83 | // Simulate response after a short delay 84 | time.Sleep(10 * time.Millisecond) 85 | msgs := transport.getMessages() 86 | if len(msgs) == 0 { 87 | t.Error("No messages sent") 88 | return 89 | } 90 | 91 | lastMsg := msgs[len(msgs)-1] 92 | req, ok := lastMsg.(map[string]interface{}) 93 | if !ok { 94 | t.Error("Last message is not a request") 95 | return 96 | } 97 | 98 | // Simulate response 99 | transport.simulateMessage(map[string]interface{}{ 100 | "jsonrpc": "2.0", 101 | "id": req["id"], 102 | "result": "test result", 103 | }) 104 | }() 105 | 106 | result, err := p.Request(ctx, "test_method", map[string]string{"key": "value"}, nil) 107 | if err != nil { 108 | t.Fatalf("Request failed: %v", err) 109 | } 110 | 111 | if result != "test result" { 112 | t.Errorf("Expected result 'test result', got %v", result) 113 | } 114 | }) 115 | 116 | // Test request timeout 117 | t.Run("Request timeout", func(t *testing.T) { 118 | ctx := context.Background() 119 | opts := &RequestOptions{ 120 | Timeout: 50 * time.Millisecond, 121 | } 122 | 123 | _, err := p.Request(ctx, "test_method", nil, opts) 124 | if err == nil { 125 | t.Fatal("Expected timeout error, got nil") 126 | } 127 | }) 128 | 129 | // Test request cancellation 130 | t.Run("Request cancellation", func(t *testing.T) { 131 | ctx, cancel := context.WithCancel(context.Background()) 132 | 133 | go func() { 134 | time.Sleep(10 * time.Millisecond) 135 | cancel() 136 | }() 137 | 138 | _, err := p.Request(ctx, "test_method", nil, nil) 139 | if !errors.Is(err, context.Canceled) { 140 | t.Fatalf("Expected context.Canceled error, got %v", err) 141 | } 142 | }) 143 | } 144 | } 145 | ``` 146 | 147 | #### Integration Tests 148 | 149 | Run integration tests that use the actual transport layers: 150 | 151 | ```go 152 | func TestWithStdioTransport(t *testing.T) { 153 | transport := stdio.NewStdioServerTransport() 154 | server := server.NewServer(transport) 155 | 156 | // Test server with real transport 157 | } 158 | ``` 159 | 160 | ### Contributing 161 | 162 | 1. Fork the repository 163 | 2. Create a feature branch 164 | 3. Make your changes 165 | 4. Add tests for new functionality 166 | 5. Run existing tests 167 | 6. Submit a pull request 168 | 169 | #### Pull Request Guidelines 170 | 171 | - Keep changes focused and atomic 172 | - Follow existing code style 173 | - Include tests for new functionality 174 | - Update documentation as needed 175 | - Add yourself to CONTRIBUTORS.md 176 | 177 | ## Adding docs 178 | 179 | 180 | 181 | **Prerequisite**: Please install Node.js (version 19 or higher) before proceeding. 182 | 183 | 184 | Follow these steps to install and run Mintlify on your operating system: 185 | 186 | **Step 1**: Install Mintlify: 187 | 188 | 189 | 190 | ```bash npm 191 | npm i -g mintlify 192 | ``` 193 | 194 | ```bash yarn 195 | yarn global add mintlify 196 | ``` 197 | 198 | 199 | 200 | **Step 2**: Navigate to the docs directory (where the `mint.json` file is located) and execute the following command: 201 | 202 | ```bash 203 | mintlify dev 204 | ``` 205 | 206 | A local preview of your documentation will be available at `http://localhost:3000`. 207 | 208 | When your PR merges into the main branch, it will be deployed automatically. 209 | 210 | 211 | ## Getting Help 212 | 213 | - Check existing [GitHub issues](https://github.com/metoro-io/mcp-golang/issues) 214 | - Join our [Discord community](https://discord.gg/33saRwE3pT) 215 | - Read the [Model Context Protocol specification](https://modelcontextprotocol.io/) 216 | -------------------------------------------------------------------------------- /docs/essentials/code.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Code Blocks' 3 | description: 'Display inline code and code blocks' 4 | icon: 'code' 5 | --- 6 | 7 | ## Basic 8 | 9 | ### Inline Code 10 | 11 | To denote a `word` or `phrase` as code, enclose it in backticks (`). 12 | 13 | ``` 14 | To denote a `word` or `phrase` as code, enclose it in backticks (`). 15 | ``` 16 | 17 | ### Code Block 18 | 19 | Use [fenced code blocks](https://www.markdownguide.org/extended-syntax/#fenced-code-blocks) by enclosing code in three backticks and follow the leading ticks with the programming language of your snippet to get syntax highlighting. Optionally, you can also write the name of your code after the programming language. 20 | 21 | ```java HelloWorld.java 22 | class HelloWorld { 23 | public static void main(String[] args) { 24 | System.out.println("Hello, World!"); 25 | } 26 | } 27 | ``` 28 | 29 | ````md 30 | ```java HelloWorld.java 31 | class HelloWorld { 32 | public static void main(String[] args) { 33 | System.out.println("Hello, World!"); 34 | } 35 | } 36 | ``` 37 | ```` 38 | -------------------------------------------------------------------------------- /docs/essentials/images.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Images and Embeds' 3 | description: 'Add image, video, and other HTML elements' 4 | icon: 'image' 5 | --- 6 | 7 | 11 | 12 | ## Image 13 | 14 | ### Using Markdown 15 | 16 | The [markdown syntax](https://www.markdownguide.org/basic-syntax/#images) lets you add images using the following code 17 | 18 | ```md 19 | ![title](/path/image.jpg) 20 | ``` 21 | 22 | Note that the image file size must be less than 5MB. Otherwise, we recommend hosting on a service like [Cloudinary](https://cloudinary.com/) or [S3](https://aws.amazon.com/s3/). You can then use that URL and embed. 23 | 24 | ### Using Embeds 25 | 26 | To get more customizability with images, you can also use [embeds](/writing-content/embed) to add images 27 | 28 | ```html 29 | 30 | ``` 31 | 32 | ## Embeds and HTML elements 33 | 34 | 44 | 45 |
46 | 47 | 48 | 49 | Mintlify supports [HTML tags in Markdown](https://www.markdownguide.org/basic-syntax/#html). This is helpful if you prefer HTML tags to Markdown syntax, and lets you create documentation with infinite flexibility. 50 | 51 | 52 | 53 | ### iFrames 54 | 55 | Loads another HTML page within the document. Most commonly used for embedding videos. 56 | 57 | ```html 58 | 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/essentials/markdown.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Markdown Syntax' 3 | description: 'Text, title, and styling in standard markdown' 4 | icon: 'text-size' 5 | --- 6 | 7 | ## Titles 8 | 9 | Best used for section headers. 10 | 11 | ```md 12 | ## Titles 13 | ``` 14 | 15 | ### Subtitles 16 | 17 | Best use to subsection headers. 18 | 19 | ```md 20 | ### Subtitles 21 | ``` 22 | 23 | 24 | 25 | Each **title** and **subtitle** creates an anchor and also shows up on the table of contents on the right. 26 | 27 | 28 | 29 | ## Text Formatting 30 | 31 | We support most markdown formatting. Simply add `**`, `_`, or `~` around text to format it. 32 | 33 | | Style | How to write it | Result | 34 | | ------------- | ----------------- | --------------- | 35 | | Bold | `**bold**` | **bold** | 36 | | Italic | `_italic_` | _italic_ | 37 | | Strikethrough | `~strikethrough~` | ~strikethrough~ | 38 | 39 | You can combine these. For example, write `**_bold and italic_**` to get **_bold and italic_** text. 40 | 41 | You need to use HTML to write superscript and subscript text. That is, add `` or `` around your text. 42 | 43 | | Text Size | How to write it | Result | 44 | | ----------- | ------------------------ | ---------------------- | 45 | | Superscript | `superscript` | superscript | 46 | | Subscript | `subscript` | subscript | 47 | 48 | ## Linking to Pages 49 | 50 | You can add a link by wrapping text in `[]()`. You would write `[link to google](https://google.com)` to [link to google](https://google.com). 51 | 52 | Links to pages in your docs need to be root-relative. Basically, you should include the entire folder path. For example, `[link to text](/writing-content/text)` links to the page "Text" in our components section. 53 | 54 | Relative links like `[link to text](../text)` will open slower because we cannot optimize them as easily. 55 | 56 | ## Blockquotes 57 | 58 | ### Singleline 59 | 60 | To create a blockquote, add a `>` in front of a paragraph. 61 | 62 | > Dorothy followed her through many of the beautiful rooms in her castle. 63 | 64 | ```md 65 | > Dorothy followed her through many of the beautiful rooms in her castle. 66 | ``` 67 | 68 | ### Multiline 69 | 70 | > Dorothy followed her through many of the beautiful rooms in her castle. 71 | > 72 | > The Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood. 73 | 74 | ```md 75 | > Dorothy followed her through many of the beautiful rooms in her castle. 76 | > 77 | > The Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood. 78 | ``` 79 | 80 | ### LaTeX 81 | 82 | Mintlify supports [LaTeX](https://www.latex-project.org) through the Latex component. 83 | 84 | 8 x (vk x H1 - H2) = (0,1) 85 | 86 | ```md 87 | 8 x (vk x H1 - H2) = (0,1) 88 | ``` 89 | -------------------------------------------------------------------------------- /docs/essentials/navigation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Navigation' 3 | description: 'The navigation field in mint.json defines the pages that go in the navigation menu' 4 | icon: 'map' 5 | --- 6 | 7 | The navigation menu is the list of links on every website. 8 | 9 | You will likely update `mint.json` every time you add a new page. Pages do not show up automatically. 10 | 11 | ## Navigation syntax 12 | 13 | Our navigation syntax is recursive which means you can make nested navigation groups. You don't need to include `.mdx` in page names. 14 | 15 | 16 | 17 | ```json Regular Navigation 18 | "navigation": [ 19 | { 20 | "group": "Getting Started", 21 | "pages": ["quickstart"] 22 | } 23 | ] 24 | ``` 25 | 26 | ```json Nested Navigation 27 | "navigation": [ 28 | { 29 | "group": "Getting Started", 30 | "pages": [ 31 | "quickstart", 32 | { 33 | "group": "Nested Reference Pages", 34 | "pages": ["nested-reference-page"] 35 | } 36 | ] 37 | } 38 | ] 39 | ``` 40 | 41 | 42 | 43 | ## Folders 44 | 45 | Simply put your MDX files in folders and update the paths in `mint.json`. 46 | 47 | For example, to have a page at `https://yoursite.com/your-folder/your-page` you would make a folder called `your-folder` containing an MDX file called `your-page.mdx`. 48 | 49 | 50 | 51 | You cannot use `api` for the name of a folder unless you nest it inside another folder. Mintlify uses Next.js which reserves the top-level `api` folder for internal server calls. A folder name such as `api-reference` would be accepted. 52 | 53 | 54 | 55 | ```json Navigation With Folder 56 | "navigation": [ 57 | { 58 | "group": "Group Name", 59 | "pages": ["your-folder/your-page"] 60 | } 61 | ] 62 | ``` 63 | 64 | ## Hidden Pages 65 | 66 | MDX files not included in `mint.json` will not show up in the sidebar but are accessible through the search bar and by linking directly to them. 67 | -------------------------------------------------------------------------------- /docs/essentials/reusable-snippets.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Reusable Snippets 3 | description: Reusable, custom snippets to keep content in sync 4 | icon: 'recycle' 5 | --- 6 | 7 | import SnippetIntro from '/snippets/snippet-intro.mdx'; 8 | 9 | 10 | 11 | ## Creating a custom snippet 12 | 13 | **Pre-condition**: You must create your snippet file in the `snippets` directory. 14 | 15 | 16 | Any page in the `snippets` directory will be treated as a snippet and will not 17 | be rendered into a standalone page. If you want to create a standalone page 18 | from the snippet, import the snippet into another file and call it as a 19 | component. 20 | 21 | 22 | ### Default export 23 | 24 | 1. Add content to your snippet file that you want to re-use across multiple 25 | locations. Optionally, you can add variables that can be filled in via props 26 | when you import the snippet. 27 | 28 | ```mdx snippets/my-snippet.mdx 29 | Hello world! This is my content I want to reuse across pages. My keyword of the 30 | day is {word}. 31 | ``` 32 | 33 | 34 | The content that you want to reuse must be inside the `snippets` directory in 35 | order for the import to work. 36 | 37 | 38 | 2. Import the snippet into your destination file. 39 | 40 | ```mdx destination-file.mdx 41 | --- 42 | title: My title 43 | description: My Description 44 | --- 45 | 46 | import MySnippet from '/snippets/path/to/my-snippet.mdx'; 47 | 48 | ## Header 49 | 50 | Lorem impsum dolor sit amet. 51 | 52 | 53 | ``` 54 | 55 | ### Reusable variables 56 | 57 | 1. Export a variable from your snippet file: 58 | 59 | ```mdx snippets/path/to/custom-variables.mdx 60 | export const myName = 'my name'; 61 | 62 | export const myObject = { fruit: 'strawberries' }; 63 | ``` 64 | 65 | 2. Import the snippet from your destination file and use the variable: 66 | 67 | ```mdx destination-file.mdx 68 | --- 69 | title: My title 70 | description: My Description 71 | --- 72 | 73 | import { myName, myObject } from '/snippets/path/to/custom-variables.mdx'; 74 | 75 | Hello, my name is {myName} and I like {myObject.fruit}. 76 | ``` 77 | 78 | ### Reusable components 79 | 80 | 1. Inside your snippet file, create a component that takes in props by exporting 81 | your component in the form of an arrow function. 82 | 83 | ```mdx snippets/custom-component.mdx 84 | export const MyComponent = ({ title }) => ( 85 |
86 |

{title}

87 |

... snippet content ...

88 |
89 | ); 90 | ``` 91 | 92 | 93 | MDX does not compile inside the body of an arrow function. Stick to HTML 94 | syntax when you can or use a default export if you need to use MDX. 95 | 96 | 97 | 2. Import the snippet into your destination file and pass in the props 98 | 99 | ```mdx destination-file.mdx 100 | --- 101 | title: My title 102 | description: My Description 103 | --- 104 | 105 | import { MyComponent } from '/snippets/custom-component.mdx'; 106 | 107 | Lorem ipsum dolor sit amet. 108 | 109 | 110 | ``` 111 | -------------------------------------------------------------------------------- /docs/favicon.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 | -------------------------------------------------------------------------------- /docs/images/checks-passed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metoro-io/mcp-golang/6598b3d737ebf5246e30df49aa2d0daf7f04fd17/docs/images/checks-passed.png -------------------------------------------------------------------------------- /docs/images/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/images/tools-example-usage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metoro-io/mcp-golang/6598b3d737ebf5246e30df49aa2d0daf7f04fd17/docs/images/tools-example-usage.png -------------------------------------------------------------------------------- /docs/introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: 'Welcome to mcp-golang - A Go Implementation of the Model Context Protocol' 4 | --- 5 | 6 | MCP Golang Light Logo 11 | MCP Golang Dark Logo 16 | 17 | ## What is mcp-golang? 18 | 19 | mcp-golang is an unofficial implementation of the [Model Context Protocol](https://modelcontextprotocol.io/) in Go. It provides a robust framework for building servers that can interact with AI models through a standardized protocol. 20 | 21 | ## Key Features 22 | 23 | 24 | 28 | Set up an MCP server with support for tools, resources, and prompts in just a few lines of code. 29 | 30 | 34 | Full Go type safety with automatic JSON schema generation from go structs. 35 | 36 | 40 | Just take the components you need: transport, protocol or server. 41 | 42 | 46 | mcp-golang has implemented the default transports: stdio and sse. If you need to implement your own transport, no problem! Use the rest of the library 47 | 48 | 49 | 50 | 51 | ## Design Philosophy 52 | 53 | The library is designed with the following principles in mind: 54 | 55 | - **Simple API**: Easy to use for basic cases while supporting complex production use cases 56 | - **Sane Defaults**: Provides reasonable defaults while allowing customization 57 | - **Server First**: Primary focus on server implementation with future plans for client support 58 | - **Production Ready**: Built for reliability and performance in production environments 59 | 60 | ## Getting Started 61 | 62 | To start using mcp-golang in your project, head over to our [Quickstart](/quickstart) guide. For more detailed information about development and contribution, check out our [Development](/development) guide. 63 | -------------------------------------------------------------------------------- /docs/logo/light.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 | -------------------------------------------------------------------------------- /docs/logo/mcp-golang-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/logo/mcp-golang-light.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/mint.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://mintlify.com/schema.json", 3 | "name": "mcp-golang", 4 | "logo": { 5 | "dark": "/logo/mcp-golang-dark.svg", 6 | "light": "/logo/mcp-golang-light.svg" 7 | }, 8 | "feedback": { 9 | "suggestEdit": true, 10 | "thumbsRating": true 11 | }, 12 | "favicon": "/images/favicon.svg", 13 | "colors": { 14 | "primary": "#00ADD8", 15 | "light": "#00ADD8", 16 | "dark": "#00ADD8", 17 | "anchors": { 18 | "from": "#00ADD8", 19 | "to": "#00ADD8" 20 | } 21 | }, 22 | "topbarCtaButton": { 23 | "name": "Github Repo", 24 | "url": "https://github.com/metoro-io/mcp-golang" 25 | }, 26 | "anchors": [ 27 | { 28 | "name": "Github", 29 | "icon": "github", 30 | "url": "https://github.com/metoro-io/mcp-golang" 31 | }, 32 | { 33 | "name": "Discord Community", 34 | "icon": "discord", 35 | "url": "https://discord.gg/33saRwE3pT" 36 | } 37 | ], 38 | "navigation": [ 39 | { 40 | "group": "Get Started", 41 | "pages": [ 42 | "introduction", 43 | "quickstart", 44 | "contributing", 45 | "architecture" 46 | ] 47 | }, 48 | { 49 | "group": "Usage Guide", 50 | "pages": [ 51 | "client", 52 | "tools" 53 | ] 54 | }, 55 | { 56 | "group": "Protocol Features", 57 | "pages": [ 58 | "change-notifications", 59 | "pagination" 60 | ] 61 | } 62 | ], 63 | "footerSocials": { 64 | "x": "https://x.com/metoro_ai", 65 | "github": "https://github.com/metoro-io/mcp-golang", 66 | "website": "https://https://metoro.io/" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /docs/pagination.mdx: -------------------------------------------------------------------------------- 1 | # Pagination 2 | 3 | MCP-Golang supports cursor-based pagination for listing tools, prompts, and resources. This allows clients to retrieve data in manageable chunks rather than all at once. 4 | 5 | By default, pagination is disabled, but you can enable it with the `WithPaginationLimit` option. 6 | 7 | As of 2024-12-13, it looks like Claude does not support pagination yet. 8 | 9 | ## How Pagination Works 10 | 11 | If pagination is enabled, the server will limit the number of items returned in each response to the specified limit. 12 | 13 | 1. Limit the number of items returned to the specified limit 14 | 2. Include a `nextCursor` in the response if there are more items available 15 | 3. Accept a `cursor` parameter in subsequent requests to get the next page 16 | 17 | ## Enabling Pagination 18 | 19 | Pagination is enabled by default with a limit of 2 items per page. You can modify this behavior when creating a new server: 20 | 21 | ```go 22 | server := mcp_golang.NewServer( 23 | mcp_golang.WithPaginationLimit(5), // Set items per page to 5 24 | ) 25 | ``` 26 | 27 | To disable pagination entirely, set the pagination limit to nil: 28 | 29 | ```go 30 | server := mcp_golang.NewServer( 31 | mcp_golang.WithPaginationLimit(nil), // Disable pagination 32 | ) 33 | ``` 34 | 35 | ## Important Notes 36 | 37 | 1. The cursor is opaque and should be treated as a black box by clients 38 | 2. Cursors are valid only for the specific list operation they were generated for 39 | 3. As of 2024-12-13, Claude does not support pagination yet 40 | 4. The pagination limit applies to all list operations (tools, prompts, and resources) -------------------------------------------------------------------------------- /docs/quickstart.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Quickstart' 3 | description: 'Set up your first mcp-golang server' 4 | --- 5 | 6 | ## Installation 7 | 8 | First, add mcp-golang to your project: 9 | 10 | ```bash 11 | go get github.com/metoro-io/mcp-golang 12 | ``` 13 | 14 | ## Basic Usage 15 | 16 | Here's a simple example of creating an MCP server with a basic tool, a prompt and two resources. 17 | 18 | ```go 19 | package main 20 | 21 | import ( 22 | "fmt" 23 | "github.com/metoro-io/mcp-golang" 24 | "github.com/metoro-io/mcp-golang/transport/stdio" 25 | ) 26 | 27 | type Content struct { 28 | Title string `json:"title" jsonschema:"required,description=The title to submit"` 29 | Description *string `json:"description" jsonschema:"description=The description to submit"` 30 | } 31 | type MyFunctionsArguments struct { 32 | Submitter string `json:"submitter" jsonschema:"required,description=The name of the thing calling this tool (openai, google, claude, etc)"` 33 | Content Content `json:"content" jsonschema:"required,description=The content of the message"` 34 | } 35 | 36 | func main() { 37 | done := make(chan struct{}) 38 | 39 | server := mcp_golang.NewServer(stdio.NewStdioServerTransport()) 40 | err := server.RegisterTool("hello", "Say hello to a person", func(arguments MyFunctionsArguments) (*mcp_golang.ToolResponse, error) { 41 | return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Hello, %s!", arguments.Submitter))), nil 42 | }) 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | err = server.RegisterPrompt("prompt_test", "This is a test prompt", func(arguments Content) (*mcp_golang.PromptResponse, error) { 48 | return mcp_golang.NewPromptResponse("description", mcp_golang.NewPromptMessage(mcp_golang.NewTextContent(fmt.Sprintf("Hello, %s!", arguments.Title)), mcp_golang.RoleUser)), nil 49 | }) 50 | if err != nil { 51 | panic(err) 52 | } 53 | 54 | err = server.RegisterResource("test://resource", "resource_test", "This is a test resource", "application/json", func() (*mcp_golang.ResourceResponse, error) { 55 | return mcp_golang.NewResourceResponse(mcp_golang.NewTextEmbeddedResource("test://resource", "This is a test resource", "application/json")), nil 56 | }) 57 | 58 | err = server.RegisterResource("file://app_logs", "app_logs", "The app logs", "text/plain", func() (*mcp_golang.ResourceResponse, error) { 59 | return mcp_golang.NewResourceResponse(mcp_golang.NewTextEmbeddedResource("file://app_logs", "This is a test resource", "text/plain")), nil 60 | }) 61 | 62 | err = server.Serve() 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | <-done 68 | } 69 | ``` 70 | 71 | ### Using with Claude 72 | Create a file in ~/Library/Application Support/Claude/claude_desktop_config.json with the following contents: 73 | 74 | ```json 75 | { 76 | "mcpServers": { 77 | "golang-mcp-server": { 78 | "command": "", 79 | "args": [], 80 | "env": {} 81 | } 82 | } 83 | } 84 | ``` 85 | 86 | ## HTTP Server Example 87 | 88 | You can also create an HTTP-based MCP server. Note that HTTP transport is stateless and doesn't support bidirectional features like notifications - use stdio transport if you need those features. 89 | 90 | ```go 91 | package main 92 | 93 | import ( 94 | "context" 95 | "log" 96 | "github.com/metoro-io/mcp-golang" 97 | "github.com/metoro-io/mcp-golang/transport/http" 98 | ) 99 | 100 | func main() { 101 | // Create an HTTP transport 102 | transport := http.NewHTTPTransport("/mcp") 103 | transport.WithAddr(":8080") 104 | 105 | // Create server with the HTTP transport 106 | server := mcp.NewServer(transport) 107 | 108 | // Register your tools 109 | server.RegisterTool("hello", &HelloTool{}) 110 | 111 | // Start the server 112 | if err := server.Serve(); err != nil { 113 | log.Fatal(err) 114 | } 115 | } 116 | ``` 117 | 118 | Or using the Gin framework: 119 | 120 | ```go 121 | package main 122 | 123 | import ( 124 | "github.com/gin-gonic/gin" 125 | "github.com/metoro-io/mcp-golang" 126 | "github.com/metoro-io/mcp-golang/transport/http" 127 | ) 128 | 129 | func main() { 130 | // Create a Gin transport 131 | transport := http.NewGinTransport() 132 | 133 | // Create server with the Gin transport 134 | server := mcp.NewServer(transport) 135 | 136 | // Register your tools 137 | server.RegisterTool("hello", &HelloTool{}) 138 | 139 | // Set up Gin router 140 | router := gin.Default() 141 | router.POST("/mcp", transport.Handler()) 142 | 143 | // Start the server 144 | router.Run(":8080") 145 | } 146 | ``` 147 | 148 | ## HTTP Client Example 149 | 150 | To connect to an HTTP-based MCP server: 151 | 152 | ```go 153 | package main 154 | 155 | import ( 156 | "context" 157 | "log" 158 | "github.com/metoro-io/mcp-golang" 159 | "github.com/metoro-io/mcp-golang/transport/http" 160 | ) 161 | 162 | func main() { 163 | // Create an HTTP client transport 164 | transport := http.NewHTTPClientTransport("/mcp") 165 | transport.WithBaseURL("http://localhost:8080") 166 | 167 | // Create client with the HTTP transport 168 | client := mcp.NewClient(transport) 169 | 170 | // Use the client with context 171 | ctx := context.Background() 172 | response, err := client.CallTool(ctx, "hello", map[string]interface{}{ 173 | "submitter": "openai", 174 | }) 175 | if err != nil { 176 | log.Fatal(err) 177 | } 178 | log.Printf("Response: %v", response) 179 | } 180 | ``` 181 | 182 | ## Next Steps 183 | 184 | - If you're interested in contributing to mcp-golang, check out [Development Guide](/development) for more detailed information 185 | - Join our [Discord Community](https://discord.gg/33saRwE3pT) for support 186 | - Visit our [GitHub Repository](https://github.com/metoro-io/mcp-golang) to contribute 187 | -------------------------------------------------------------------------------- /docs/snippets/snippet-intro.mdx: -------------------------------------------------------------------------------- 1 | One of the core principles of software development is DRY (Don't Repeat 2 | Yourself). This is a principle that apply to documentation as 3 | well. If you find yourself repeating the same content in multiple places, you 4 | should consider creating a custom snippet to keep your content in sync. 5 | -------------------------------------------------------------------------------- /docs/tools.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Tools' 3 | description: 'Using tools in mcp-golang' 4 | --- 5 | ## What is a tool? 6 | 7 | A tool as defined in the MCP protocol is: 8 | 9 | > The Model Context Protocol (MCP) allows servers to expose tools that can be invoked by language models. Tools enable models to interact with external systems, such as querying databases, calling APIs, or performing computations. Each tool is uniquely identified by a name and includes metadata describing its schema. 10 | 11 | In MCP golang, you can register a tool with the MCP server using the `RegisterTool` function. This function takes a name, a description, and a handler that will be called when the tool is called by a client. 12 | 13 | Here's an example of spinning up a server that has a single tool: 14 | 15 | ```go 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | "github.com/metoro-io/mcp-golang" 21 | "github.com/metoro-io/mcp-golang/transport/stdio" 22 | ) 23 | 24 | type HelloArguments struct { 25 | Submitter string `json:"submitter" jsonschema:"required,description=The name of the thing calling this tool (openai or google or claude etc)'"` 26 | } 27 | 28 | func main() { 29 | done := make(chan struct{}) 30 | server := mcp_golang.NewServer(stdio.NewStdioServerTransport()) 31 | err := server.RegisterTool("hello", "Say hello to a person", func(arguments HelloArguments) (*mcp_golang.ToolResponse, error) { 32 | return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Hello, %s!", arguments.Submitter))), nil 33 | }) 34 | err = server.Serve() 35 | if err != nil { 36 | panic(err) 37 | } 38 | <-done 39 | } 40 | 41 | ``` 42 | 43 | There are a few things going on in the tool registration that are worth mentioning: 44 | 1. The `RegisterTool` function takes a name, a description, and a handler that will be called when the tool is called by a client. The information you pass to the `RegisterTool` function is used to generate the tool schema. 45 | When a client calls a tool, the server will send the arguments to the handler function. 46 | 2. The arguments of the handler function must be a single struct. That struct can be anything you like, golang-mcp will take care of serializing and deserializing the arguments to and from JSON. 47 | The struct you use should have valid json and jsonschema tags. These will also be used to populate the tool schema. 48 | 3. The return values of the handler must be a `*mcp_golang.ToolResponse` and an `error`. If you pass back an error, mcp-golang will take care of serializing it and passing it back to the client. 49 | 50 | ### Schema Generation 51 | 52 | One of the main features of mcp-golang is the ability to automatically generate the schema for tools. This is done by inspecting the arguments and return values of the handler function. 53 | You don't have to worry about maintaining a schema manually. Just make sure your input struct is up to date and mcp-golang will take care of the rest. 54 | 55 | For the example above, this is what the mcp-protocol messages will look like 56 | ```json 57 | client: {"method":"tools/list","params":{},"jsonrpc":"2.0","id":2} 58 | server: {"id":2,"jsonrpc":"2.0","result":{"tools":[{"description":"Say hello to a person","inputSchema":{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{"submitter":{"type":"string","description":"The name of the thing calling this tool (openai or google or claude etc)'"}},"type":"object","required":["submitter"]},"name":"hello"}]}} 59 | ``` 60 | 61 | Using this function in claude, looks like this: 62 | 63 | Simple Tool usage mcp-golang in claude 64 | 65 | The underlying rpc messages for the call itself look like this: 66 | 67 | ```json 68 | client: {"method":"tools/call","params":{"name":"hello","arguments":{"submitter":"claude"}},"jsonrpc":"2.0","id":10} 69 | server: {"id":10,"jsonrpc":"2.0","result":{"content":[{"text":"Hello, claude!","type":"text"}],"isError":false}} 70 | ``` 71 | 72 | 73 | ### Tool Arguments 74 | 75 | * **Required fields** If you need the client to always provide this argument, use the `jsonschema:"required"` tag. 76 | * **Optional fields** All fields are optional by default. Just don't use the `jsonschema:"required"` tag. 77 | * **Description** Use the `jsonschema:"description"` tag to add a description to the argument. 78 | 79 | ## HTTP Transport 80 | 81 | The MCP SDK now supports HTTP transport for both client and server implementations. This allows you to build MCP tools that communicate over HTTP/HTTPS endpoints. 82 | 83 | **Note:** The HTTP transport implementations are stateless, which means they don't support bidirectional communication features like notifications. If you need to send notifications or maintain a persistent connection between client and server, use the stdio transport instead. 84 | 85 | ### HTTP Server 86 | 87 | There are two server implementations available: 88 | 89 | 1. Standard HTTP Server: 90 | ```go 91 | transport := http.NewHTTPTransport("/mcp") 92 | transport.WithAddr(":8080") // Optional, defaults to :8080 93 | ``` 94 | 95 | 2. Gin Framework Server: 96 | ```go 97 | transport := http.NewGinTransport() 98 | router := gin.Default() 99 | router.POST("/mcp", transport.Handler()) 100 | ``` 101 | 102 | ### HTTP Client 103 | 104 | The HTTP client transport allows you to connect to MCP servers over HTTP: 105 | 106 | ```go 107 | transport := http.NewHTTPClientTransport("/mcp") 108 | transport.WithBaseURL("http://localhost:8080") 109 | ``` 110 | 111 | ### Context Support 112 | 113 | All transport implementations now support context propagation. This allows you to pass request-scoped data and handle timeouts/cancellation: 114 | 115 | ```go 116 | transport.SetMessageHandler(func(ctx context.Context, msg *transport.BaseJsonRpcMessage) { 117 | // Access context values or handle cancellation 118 | if deadline, ok := ctx.Deadline(); ok { 119 | // Handle deadline 120 | } 121 | // Process message 122 | }) 123 | ``` 124 | 125 | The context is propagated through all MCP operations, making it easier to implement timeouts, tracing, and other cross-cutting concerns. 126 | 127 | 128 | -------------------------------------------------------------------------------- /examples/basic_tool_server/basic_tool_server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/metoro-io/mcp-golang" 6 | "github.com/metoro-io/mcp-golang/transport/stdio" 7 | ) 8 | 9 | type Content struct { 10 | Title string `json:"title" jsonschema:"required,description=The title to submit"` 11 | Description *string `json:"description" jsonschema:"description=The description to submit"` 12 | } 13 | type MyFunctionsArguments struct { 14 | Submitter string `json:"submitter" jsonschema:"required,description=The name of the thing calling this tool (openai, google, claude, etc)"` 15 | Content Content `json:"content" jsonschema:"required,description=The content of the message"` 16 | } 17 | 18 | func main() { 19 | done := make(chan struct{}) 20 | 21 | server := mcp_golang.NewServer(stdio.NewStdioServerTransport()) 22 | err := server.RegisterTool("hello", "Say hello to a person", func(arguments MyFunctionsArguments) (*mcp_golang.ToolResponse, error) { 23 | return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Hello, %server!", arguments.Submitter))), nil 24 | }) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | err = server.RegisterPrompt("prompt_test", "This is a test prompt", func(arguments Content) (*mcp_golang.PromptResponse, error) { 30 | return mcp_golang.NewPromptResponse("description", mcp_golang.NewPromptMessage(mcp_golang.NewTextContent(fmt.Sprintf("Hello, %server!", arguments.Title)), mcp_golang.RoleUser)), nil 31 | }) 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | err = server.RegisterResource("test://resource", "resource_test", "This is a test resource", "application/json", func() (*mcp_golang.ResourceResponse, error) { 37 | return mcp_golang.NewResourceResponse(mcp_golang.NewTextEmbeddedResource("test://resource", "This is a test resource", "application/json")), nil 38 | }) 39 | 40 | err = server.RegisterResource("file://app_logs", "app_logs", "The app logs", "text/plain", func() (*mcp_golang.ResourceResponse, error) { 41 | return mcp_golang.NewResourceResponse(mcp_golang.NewTextEmbeddedResource("file://app_logs", "This is a test resource", "text/plain")), nil 42 | }) 43 | 44 | err = server.Serve() 45 | if err != nil { 46 | panic(err) 47 | } 48 | 49 | <-done 50 | } 51 | -------------------------------------------------------------------------------- /examples/client/README.md: -------------------------------------------------------------------------------- 1 | # MCP Client Example 2 | 3 | This example demonstrates how to use the Model Context Protocol (MCP) client to interact with an MCP server. The example includes both a client and a server implementation, showcasing various MCP features like tools and prompts. 4 | 5 | ## Features Demonstrated 6 | 7 | - Client initialization and connection to server 8 | - Listing available tools 9 | - Calling different tools: 10 | - Hello tool: Basic greeting functionality 11 | - Calculate tool: Simple arithmetic operations 12 | - Time tool: Current time formatting 13 | - Listing available prompts 14 | - Using prompts: 15 | - Uppercase prompt: Converts text to uppercase 16 | - Reverse prompt: Reverses input text 17 | 18 | ## Running the Example 19 | 20 | 1. Make sure you're in the `examples/client` directory: 21 | ```bash 22 | cd examples/client 23 | ``` 24 | 25 | 2. Run the example: 26 | ```bash 27 | go run main.go 28 | ``` 29 | 30 | The program will: 31 | 1. Start a local MCP server (implemented in `server/main.go`) 32 | 2. Create an MCP client and connect to the server 33 | 3. Demonstrate various interactions with the server 34 | 35 | ## Expected Output 36 | 37 | You should see output similar to this: 38 | 39 | ``` 40 | Available Tools: 41 | Tool: hello. Description: A simple greeting tool 42 | Tool: calculate. Description: A basic calculator 43 | Tool: time. Description: Returns formatted current time 44 | 45 | Calling hello tool: 46 | Hello response: Hello, World! 47 | 48 | Calling calculate tool: 49 | Calculate response: Result of 10 + 5 = 15 50 | 51 | Calling time tool: 52 | Time response: [current time in format: 2006-01-02 15:04:05] 53 | 54 | Available Prompts: 55 | Prompt: uppercase. Description: Converts text to uppercase 56 | Prompt: reverse. Description: Reverses the input text 57 | 58 | Calling uppercase prompt: 59 | Uppercase response: HELLO, MODEL CONTEXT PROTOCOL! 60 | 61 | Calling reverse prompt: 62 | Reverse response: !locotorP txetnoC ledoM ,olleH 63 | ``` 64 | 65 | ## Code Structure 66 | 67 | - `main.go`: Client implementation and example usage 68 | - `server/main.go`: Example MCP server implementation with sample tools and prompts 69 | 70 | ## Notes 71 | 72 | - The server is automatically started and stopped by the client program 73 | - The example uses stdio transport for communication between client and server 74 | - All tools and prompts are simple examples to demonstrate the protocol functionality -------------------------------------------------------------------------------- /examples/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os/exec" 6 | 7 | "context" 8 | 9 | mcp_golang "github.com/metoro-io/mcp-golang" 10 | "github.com/metoro-io/mcp-golang/transport/stdio" 11 | ) 12 | 13 | func main() { 14 | // Start the server process 15 | cmd := exec.Command("go", "run", "./server/main.go") 16 | stdin, err := cmd.StdinPipe() 17 | if err != nil { 18 | log.Fatalf("Failed to get stdin pipe: %v", err) 19 | } 20 | stdout, err := cmd.StdoutPipe() 21 | if err != nil { 22 | log.Fatalf("Failed to get stdout pipe: %v", err) 23 | } 24 | 25 | if err := cmd.Start(); err != nil { 26 | log.Fatalf("Failed to start server: %v", err) 27 | } 28 | defer cmd.Process.Kill() 29 | 30 | clientTransport := stdio.NewStdioServerTransportWithIO(stdout, stdin) 31 | client := mcp_golang.NewClient(clientTransport) 32 | 33 | if _, err := client.Initialize(context.Background()); err != nil { 34 | log.Fatalf("Failed to initialize client: %v", err) 35 | } 36 | 37 | // List available tools 38 | tools, err := client.ListTools(context.Background(), nil) 39 | if err != nil { 40 | log.Fatalf("Failed to list tools: %v", err) 41 | } 42 | 43 | log.Println("Available Tools:") 44 | for _, tool := range tools.Tools { 45 | desc := "" 46 | if tool.Description != nil { 47 | desc = *tool.Description 48 | } 49 | log.Printf("Tool: %s. Description: %s", tool.Name, desc) 50 | } 51 | 52 | // Example of calling the hello tool 53 | helloArgs := map[string]interface{}{ 54 | "name": "World", 55 | } 56 | 57 | log.Println("\nCalling hello tool:") 58 | helloResponse, err := client.CallTool(context.Background(), "hello", helloArgs) 59 | if err != nil { 60 | log.Printf("Failed to call hello tool: %v", err) 61 | } else if helloResponse != nil && len(helloResponse.Content) > 0 && helloResponse.Content[0].TextContent != nil { 62 | log.Printf("Hello response: %s", helloResponse.Content[0].TextContent.Text) 63 | } 64 | 65 | // Example of calling the calculate tool 66 | calcArgs := map[string]interface{}{ 67 | "operation": "add", 68 | "a": 10, 69 | "b": 5, 70 | } 71 | 72 | log.Println("\nCalling calculate tool:") 73 | calcResponse, err := client.CallTool(context.Background(), "calculate", calcArgs) 74 | if err != nil { 75 | log.Printf("Failed to call calculate tool: %v", err) 76 | } else if calcResponse != nil && len(calcResponse.Content) > 0 && calcResponse.Content[0].TextContent != nil { 77 | log.Printf("Calculate response: %s", calcResponse.Content[0].TextContent.Text) 78 | } 79 | 80 | // Example of calling the time tool 81 | timeArgs := map[string]interface{}{ 82 | "format": "2006-01-02 15:04:05", 83 | } 84 | 85 | log.Println("\nCalling time tool:") 86 | timeResponse, err := client.CallTool(context.Background(), "time", timeArgs) 87 | if err != nil { 88 | log.Printf("Failed to call time tool: %v", err) 89 | } else if timeResponse != nil && len(timeResponse.Content) > 0 && timeResponse.Content[0].TextContent != nil { 90 | log.Printf("Time response: %s", timeResponse.Content[0].TextContent.Text) 91 | } 92 | 93 | // List available prompts 94 | prompts, err := client.ListPrompts(context.Background(), nil) 95 | if err != nil { 96 | log.Printf("Failed to list prompts: %v", err) 97 | } else { 98 | log.Println("\nAvailable Prompts:") 99 | for _, prompt := range prompts.Prompts { 100 | desc := "" 101 | if prompt.Description != nil { 102 | desc = *prompt.Description 103 | } 104 | log.Printf("Prompt: %s. Description: %s", prompt.Name, desc) 105 | } 106 | 107 | // Example of using the uppercase prompt 108 | promptArgs := map[string]interface{}{ 109 | "input": "Hello, Model Context Protocol!", 110 | } 111 | 112 | log.Printf("\nCalling uppercase prompt:") 113 | upperResponse, err := client.GetPrompt(context.Background(), "uppercase", promptArgs) 114 | if err != nil { 115 | log.Printf("Failed to get uppercase prompt: %v", err) 116 | } else if upperResponse != nil && len(upperResponse.Messages) > 0 && upperResponse.Messages[0].Content != nil { 117 | log.Printf("Uppercase response: %s", upperResponse.Messages[0].Content.TextContent.Text) 118 | } 119 | 120 | // Example of using the reverse prompt 121 | log.Printf("\nCalling reverse prompt:") 122 | reverseResponse, err := client.GetPrompt(context.Background(), "reverse", promptArgs) 123 | if err != nil { 124 | log.Printf("Failed to get reverse prompt: %v", err) 125 | } else if reverseResponse != nil && len(reverseResponse.Messages) > 0 && reverseResponse.Messages[0].Content != nil { 126 | log.Printf("Reverse response: %s", reverseResponse.Messages[0].Content.TextContent.Text) 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /examples/client/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | mcp "github.com/metoro-io/mcp-golang" 9 | "github.com/metoro-io/mcp-golang/transport/stdio" 10 | ) 11 | 12 | // HelloArgs represents the arguments for the hello tool 13 | type HelloArgs struct { 14 | Name string `json:"name" jsonschema:"required,description=The name to say hello to"` 15 | } 16 | 17 | // CalculateArgs represents the arguments for the calculate tool 18 | type CalculateArgs struct { 19 | Operation string `json:"operation" jsonschema:"required,enum=add,enum=subtract,enum=multiply,enum=divide,description=The mathematical operation to perform"` 20 | A float64 `json:"a" jsonschema:"required,description=First number"` 21 | B float64 `json:"b" jsonschema:"required,description=Second number"` 22 | } 23 | 24 | // TimeArgs represents the arguments for the current time tool 25 | type TimeArgs struct { 26 | Format string `json:"format,omitempty" jsonschema:"description=Optional time format (default: RFC3339)"` 27 | } 28 | 29 | // PromptArgs represents the arguments for custom prompts 30 | type PromptArgs struct { 31 | Input string `json:"input" jsonschema:"required,description=The input text to process"` 32 | } 33 | 34 | func main() { 35 | // Create a transport for the server 36 | serverTransport := stdio.NewStdioServerTransport() 37 | 38 | // Create a new server with the transport 39 | server := mcp.NewServer(serverTransport) 40 | 41 | // Register hello tool 42 | err := server.RegisterTool("hello", "Says hello to the provided name", func(args HelloArgs) (*mcp.ToolResponse, error) { 43 | message := fmt.Sprintf("Hello, %s!", args.Name) 44 | return mcp.NewToolResponse(mcp.NewTextContent(message)), nil 45 | }) 46 | if err != nil { 47 | panic(err) 48 | } 49 | 50 | // Register calculate tool 51 | err = server.RegisterTool("calculate", "Performs basic mathematical operations", func(args CalculateArgs) (*mcp.ToolResponse, error) { 52 | var result float64 53 | switch args.Operation { 54 | case "add": 55 | result = args.A + args.B 56 | case "subtract": 57 | result = args.A - args.B 58 | case "multiply": 59 | result = args.A * args.B 60 | case "divide": 61 | if args.B == 0 { 62 | return nil, fmt.Errorf("division by zero") 63 | } 64 | result = args.A / args.B 65 | default: 66 | return nil, fmt.Errorf("unknown operation: %s", args.Operation) 67 | } 68 | message := fmt.Sprintf("Result of %s: %.2f", args.Operation, result) 69 | return mcp.NewToolResponse(mcp.NewTextContent(message)), nil 70 | }) 71 | if err != nil { 72 | panic(err) 73 | } 74 | 75 | // Register current time tool 76 | err = server.RegisterTool("time", "Returns the current time", func(args TimeArgs) (*mcp.ToolResponse, error) { 77 | format := time.RFC3339 78 | if args.Format != "" { 79 | format = args.Format 80 | } 81 | message := time.Now().Format(format) 82 | return mcp.NewToolResponse(mcp.NewTextContent(message)), nil 83 | }) 84 | if err != nil { 85 | panic(err) 86 | } 87 | 88 | // Register example prompts 89 | err = server.RegisterPrompt("uppercase", "Converts text to uppercase", func(args PromptArgs) (*mcp.PromptResponse, error) { 90 | text := strings.ToUpper(args.Input) 91 | return mcp.NewPromptResponse("uppercase", mcp.NewPromptMessage(mcp.NewTextContent(text), mcp.RoleUser)), nil 92 | }) 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | err = server.RegisterPrompt("reverse", "Reverses the input text", func(args PromptArgs) (*mcp.PromptResponse, error) { 98 | // Reverse the string 99 | runes := []rune(args.Input) 100 | for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { 101 | runes[i], runes[j] = runes[j], runes[i] 102 | } 103 | text := string(runes) 104 | return mcp.NewPromptResponse("reverse", mcp.NewPromptMessage(mcp.NewTextContent(text), mcp.RoleUser)), nil 105 | }) 106 | if err != nil { 107 | panic(err) 108 | } 109 | 110 | // Start the server 111 | if err := server.Serve(); err != nil { 112 | panic(err) 113 | } 114 | 115 | // Keep the server running 116 | select {} 117 | } 118 | -------------------------------------------------------------------------------- /examples/get_weather_tool_server/get_weather_tool_server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | mcp_golang "github.com/metoro-io/mcp-golang" 6 | "github.com/metoro-io/mcp-golang/transport/stdio" 7 | "io" 8 | "net/http" 9 | ) 10 | 11 | type WeatherArguments struct { 12 | Longitude float64 `json:"longitude" jsonschema:"required,description=The longitude of the location to get the weather for"` 13 | Latitude float64 `json:"latitude" jsonschema:"required,description=The latitude of the location to get the weather for"` 14 | } 15 | 16 | // This is explained in the docs at https://mcpgolang.com/tools 17 | func main() { 18 | done := make(chan struct{}) 19 | server := mcp_golang.NewServer(stdio.NewStdioServerTransport()) 20 | err := server.RegisterTool("get_weather", "Get the weather forecast for temperature, wind speed and relative humidity", func(arguments WeatherArguments) (*mcp_golang.ToolResponse, error) { 21 | url := fmt.Sprintf("https://api.open-meteo.com/v1/forecast?latitude=%f&longitude=%f¤t=temperature_2m,wind_speed_10m&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m", arguments.Latitude, arguments.Longitude) 22 | resp, err := http.Get(url) 23 | if err != nil { 24 | return nil, err 25 | } 26 | defer resp.Body.Close() 27 | output, err := io.ReadAll(resp.Body) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(string(output))), nil 32 | }) 33 | err = server.Serve() 34 | if err != nil { 35 | panic(err) 36 | } 37 | <-done 38 | } 39 | -------------------------------------------------------------------------------- /examples/gin_example/README.md: -------------------------------------------------------------------------------- 1 | # Gin Integration Example 2 | 3 | This example demonstrates how to integrate the MCP server with a Gin web application. It shows how to: 4 | 1. Create an MCP server with a Gin transport 5 | 2. Register tools with the server 6 | 3. Add the MCP endpoint to a Gin router 7 | 8 | ## Running the Example 9 | 10 | 1. Start the server: 11 | ```bash 12 | go run main.go 13 | ``` 14 | This will start a Gin server on port 8081 with an MCP endpoint at `/mcp`. 15 | 16 | 2. You can test it using the HTTP client example: 17 | ```bash 18 | cd ../http_example 19 | go run client/main.go 20 | ``` 21 | 22 | ## Understanding the Code 23 | 24 | The key components are: 25 | 26 | 1. `GinTransport`: A transport implementation that works with Gin's router 27 | 2. `Handler()`: Returns a Gin handler function that can be used with any Gin router 28 | 3. Tool Registration: Shows how to register tools that can be called via the MCP endpoint 29 | 30 | ## Integration with Existing Gin Applications 31 | 32 | To add MCP support to your existing Gin application: 33 | 34 | ```go 35 | // Create the transport 36 | transport := http.NewGinTransport() 37 | 38 | // Create the MCP server 39 | server := mcp_golang.NewServer(transport) 40 | 41 | // Register your tools 42 | server.RegisterTool("mytool", "Tool description", myToolHandler) 43 | 44 | // Start the server 45 | go server.Serve() 46 | 47 | // Add the MCP endpoint to your Gin router 48 | router.POST("/mcp", transport.Handler()) 49 | ``` -------------------------------------------------------------------------------- /examples/gin_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | mcp_golang "github.com/metoro-io/mcp-golang" 11 | "github.com/metoro-io/mcp-golang/transport/http" 12 | ) 13 | 14 | // TimeArgs defines the arguments for the time tool 15 | type TimeArgs struct { 16 | Format string `json:"format" jsonschema:"description=The time format to use"` 17 | } 18 | 19 | func main() { 20 | // Create a Gin transport 21 | transport := http.NewGinTransport() 22 | 23 | // Create a new server with the transport 24 | server := mcp_golang.NewServer(transport, mcp_golang.WithName("mcp-golang-gin-example"), mcp_golang.WithVersion("0.0.1")) 25 | 26 | // Register a simple tool 27 | err := server.RegisterTool("time", "Returns the current time in the specified format", func(ctx context.Context, args TimeArgs) (*mcp_golang.ToolResponse, error) { 28 | ginCtx, ok := ctx.Value("ginContext").(*gin.Context) 29 | if !ok { 30 | return nil, fmt.Errorf("ginContext not found in context") 31 | } 32 | userAgent := ginCtx.GetHeader("User-Agent") 33 | log.Printf("Request from User-Agent: %s", userAgent) 34 | 35 | format := args.Format 36 | if format == "" { 37 | format = time.RFC3339 38 | } 39 | return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(time.Now().Format(format))), nil 40 | }) 41 | if err != nil { 42 | panic(err) 43 | } 44 | 45 | go server.Serve() 46 | 47 | // Create a Gin router 48 | r := gin.Default() 49 | 50 | // Add the MCP endpoint 51 | r.POST("/mcp", transport.Handler()) 52 | 53 | // Start the server 54 | log.Println("Starting Gin server on :8081...") 55 | if err := r.Run(":8081"); err != nil { 56 | log.Fatalf("Server error: %v", err) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/http_example/README.md: -------------------------------------------------------------------------------- 1 | # HTTP Transport Example 2 | 3 | This example demonstrates how to use the HTTP transport in MCP. It consists of a server that provides a simple time tool and a client that connects to it. 4 | 5 | ## Running the Example 6 | 7 | 1. First, start the server: 8 | ```bash 9 | go run server/main.go 10 | ``` 11 | This will start an HTTP server on port 8080. 12 | 13 | 2. In another terminal, run the client: 14 | ```bash 15 | go run client/main.go 16 | ``` 17 | 18 | The client will: 19 | 1. Connect to the server 20 | 2. List available tools 21 | 3. Call the time tool with different time formats 22 | 4. Display the results 23 | 24 | ## Understanding the Code 25 | 26 | - `server/main.go`: Shows how to create an MCP server using HTTP transport and register a tool 27 | - `client/main.go`: Shows how to create an MCP client that connects to an HTTP server and calls tools 28 | 29 | The example demonstrates: 30 | - Setting up HTTP transport 31 | - Tool registration and description 32 | - Tool parameter handling 33 | - Making tool calls with different arguments 34 | - Handling tool responses -------------------------------------------------------------------------------- /examples/http_example/auth_example_client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/davecgh/go-spew/spew" 8 | mcp_golang "github.com/metoro-io/mcp-golang" 9 | "github.com/metoro-io/mcp-golang/transport/http" 10 | ) 11 | 12 | func main() { 13 | // Create an HTTP transport that connects to the server 14 | transport := http.NewHTTPClientTransport("/mcp") 15 | transport.WithBaseURL("http://localhost:8080/api/v1") 16 | // Public metoro token - not a leak 17 | transport.WithHeader("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjdXN0b21lcklkIjoiOThlZDU1M2QtYzY4ZC00MDRhLWFhZjItNDM2ODllNWJiMGUzIiwiZW1haWwiOiJ0ZXN0QGNocmlzYmF0dGFyYmVlLmNvbSIsImV4cCI6MTgyMTI0NzIzN30.QeFzKsP1yO16pVol0mkAdt7qhJf6nTqBoqXqdWawBdE") 18 | 19 | // Create a new client with the transport 20 | client := mcp_golang.NewClient(transport) 21 | 22 | // Initialize the client 23 | if resp, err := client.Initialize(context.Background()); err != nil { 24 | log.Fatalf("Failed to initialize client: %v", err) 25 | } else { 26 | log.Printf("Initialized client: %v", spew.Sdump(resp)) 27 | } 28 | 29 | // List available tools 30 | tools, err := client.ListTools(context.Background(), nil) 31 | if err != nil { 32 | log.Fatalf("Failed to list tools: %v", err) 33 | } 34 | 35 | log.Println("Available Tools:") 36 | for _, tool := range tools.Tools { 37 | desc := "" 38 | if tool.Description != nil { 39 | desc = *tool.Description 40 | } 41 | log.Printf("Tool: %s. Description: %s", tool.Name, desc) 42 | } 43 | 44 | response, err := client.CallTool(context.Background(), "get_log_attributes", map[string]interface{}{}) 45 | if err != nil { 46 | log.Fatalf("Failed to call get_log_attributes tool: %v", err) 47 | } 48 | 49 | log.Printf("Response: %v", spew.Sdump(response)) 50 | } 51 | -------------------------------------------------------------------------------- /examples/http_example/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "github.com/davecgh/go-spew/spew" 9 | mcp_golang "github.com/metoro-io/mcp-golang" 10 | "github.com/metoro-io/mcp-golang/transport/http" 11 | ) 12 | 13 | func main() { 14 | // Create an HTTP transport that connects to the server 15 | transport := http.NewHTTPClientTransport("/mcp") 16 | transport.WithBaseURL("http://localhost:8081") 17 | 18 | // Create a new client with the transport 19 | client := mcp_golang.NewClient(transport) 20 | 21 | // Initialize the client 22 | if resp, err := client.Initialize(context.Background()); err != nil { 23 | log.Fatalf("Failed to initialize client: %v", err) 24 | } else { 25 | log.Printf("Initialized client: %v", spew.Sdump(resp)) 26 | } 27 | 28 | // List available tools 29 | tools, err := client.ListTools(context.Background(), nil) 30 | if err != nil { 31 | log.Fatalf("Failed to list tools: %v", err) 32 | } 33 | 34 | log.Println("Available Tools:") 35 | for _, tool := range tools.Tools { 36 | desc := "" 37 | if tool.Description != nil { 38 | desc = *tool.Description 39 | } 40 | log.Printf("Tool: %s. Description: %s", tool.Name, desc) 41 | } 42 | 43 | // Call the time tool with different formats 44 | formats := []string{ 45 | time.RFC3339, 46 | "2006-01-02 15:04:05", 47 | "Mon, 02 Jan 2006", 48 | } 49 | 50 | for _, format := range formats { 51 | args := map[string]interface{}{ 52 | "format": format, 53 | } 54 | 55 | response, err := client.CallTool(context.Background(), "time", args) 56 | if err != nil { 57 | log.Printf("Failed to call time tool: %v", err) 58 | continue 59 | } 60 | 61 | if len(response.Content) > 0 && response.Content[0].TextContent != nil { 62 | log.Printf("Time in format %q: %s", format, response.Content[0].TextContent.Text) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/http_example/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | mcp_golang "github.com/metoro-io/mcp-golang" 8 | "github.com/metoro-io/mcp-golang/transport/http" 9 | ) 10 | 11 | // TimeArgs defines the arguments for the time tool 12 | type TimeArgs struct { 13 | Format string `json:"format" jsonschema:"description=The time format to use"` 14 | } 15 | 16 | func main() { 17 | // Create an HTTP transport that listens on /mcp endpoint 18 | transport := http.NewHTTPTransport("/mcp").WithAddr(":8081") 19 | 20 | // Create a new server with the transport 21 | server := mcp_golang.NewServer( 22 | transport, 23 | mcp_golang.WithName("mcp-golang-stateless-http-example"), 24 | mcp_golang.WithInstructions("A simple example of a stateless HTTP server using mcp-golang"), 25 | mcp_golang.WithVersion("0.0.1"), 26 | ) 27 | 28 | // Register a simple tool 29 | err := server.RegisterTool("time", "Returns the current time in the specified format", func(args TimeArgs) (*mcp_golang.ToolResponse, error) { 30 | format := args.Format 31 | return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(time.Now().Format(format))), nil 32 | }) 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | // Start the server 38 | log.Println("Starting HTTP server on :8081...") 39 | server.Serve() 40 | } 41 | -------------------------------------------------------------------------------- /examples/pagination_example/README.md: -------------------------------------------------------------------------------- 1 | # Pagination Example 2 | 3 | BEWARE: As of 2024-12-13, it looks like Claude does not support pagination yet 4 | 5 | This example demonstrates how to use pagination in mcp-golang for listing tools, prompts, and resources. 6 | 7 | ## Overview 8 | 9 | The server is configured with a pagination limit of 2 items per page and registers: 10 | - 5 tools (3 hello tools and 2 bye tools) 11 | - 5 prompts (3 greeting prompts and 2 farewell prompts) 12 | - 5 resources (text files) 13 | 14 | ## Expected Behavior 15 | 16 | 1. First page requests will return 2 items and a nextCursor 17 | 2. Using the nextCursor will fetch the next 2 items 18 | 3. The last page will have no nextCursor 19 | 4. Items are returned in alphabetical order by name (for tools and prompts) or URI (for resources) 20 | 21 | ## Implementation Details 22 | 23 | - Uses the `WithPaginationLimit` option to enable pagination 24 | - Demonstrates cursor-based pagination for all three types: tools, prompts, and resources 25 | - Shows how to handle multiple pages of results 26 | - Includes examples of proper error handling 27 | -------------------------------------------------------------------------------- /examples/pagination_example/pagination_example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | mcp_golang "github.com/metoro-io/mcp-golang" 6 | "github.com/metoro-io/mcp-golang/transport/stdio" 7 | ) 8 | 9 | // Arguments for our tools 10 | type HelloArguments struct { 11 | Name string `json:"name" jsonschema:"required,description=The name to say hello to"` 12 | } 13 | 14 | type ByeArguments struct { 15 | Name string `json:"name" jsonschema:"required,description=The name to say goodbye to"` 16 | } 17 | 18 | // Arguments for our prompts 19 | type GreetingArguments struct { 20 | Language string `json:"language" jsonschema:"required,description=The language to greet in"` 21 | } 22 | 23 | type FarewellArguments struct { 24 | Language string `json:"language" jsonschema:"required,description=The language to say farewell in"` 25 | } 26 | 27 | func main() { 28 | // Create a new server with pagination enabled (2 items per page) 29 | server := mcp_golang.NewServer( 30 | stdio.NewStdioServerTransport(), 31 | mcp_golang.WithPaginationLimit(2), 32 | ) 33 | err := server.Serve() 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | // Register multiple tools 39 | toolNames := []string{"hello1", "hello2", "hello3", "bye1", "bye2"} 40 | for _, name := range toolNames[:3] { 41 | err = server.RegisterTool(name, "Say hello to someone", func(args HelloArguments) (*mcp_golang.ToolResponse, error) { 42 | return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Hello, %s!", args.Name))), nil 43 | }) 44 | if err != nil { 45 | panic(err) 46 | } 47 | } 48 | for _, name := range toolNames[3:] { 49 | err = server.RegisterTool(name, "Say goodbye to someone", func(args ByeArguments) (*mcp_golang.ToolResponse, error) { 50 | return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Goodbye, %s!", args.Name))), nil 51 | }) 52 | if err != nil { 53 | panic(err) 54 | } 55 | } 56 | 57 | // Register multiple prompts 58 | promptNames := []string{"greet1", "greet2", "greet3", "farewell1", "farewell2"} 59 | for _, name := range promptNames[:3] { 60 | err = server.RegisterPrompt(name, "Greeting in different languages", func(args GreetingArguments) (*mcp_golang.PromptResponse, error) { 61 | return mcp_golang.NewPromptResponse("test", mcp_golang.NewPromptMessage(mcp_golang.NewTextContent(fmt.Sprintf("Hello in %s!", args.Language)), mcp_golang.RoleUser)), nil 62 | }) 63 | if err != nil { 64 | panic(err) 65 | } 66 | } 67 | for _, name := range promptNames[3:] { 68 | err = server.RegisterPrompt(name, "Farewell in different languages", func(args FarewellArguments) (*mcp_golang.PromptResponse, error) { 69 | return mcp_golang.NewPromptResponse("test", mcp_golang.NewPromptMessage(mcp_golang.NewTextContent(fmt.Sprintf("Goodbye in %s!", args.Language)), mcp_golang.RoleUser)), nil 70 | }) 71 | if err != nil { 72 | panic(err) 73 | } 74 | } 75 | 76 | // Register multiple resources 77 | resourceNames := []string{"resource1.txt", "resource2.txt", "resource3.txt", "resource4.txt", "resource5.txt"} 78 | for i, name := range resourceNames { 79 | content := fmt.Sprintf("This is resource %d", i+1) 80 | err = server.RegisterResource( 81 | name, 82 | fmt.Sprintf("Resource %d", i+1), 83 | fmt.Sprintf("Description for resource %d", i+1), 84 | "text/plain", 85 | func() (*mcp_golang.ResourceResponse, error) { 86 | return mcp_golang.NewResourceResponse(mcp_golang.NewTextEmbeddedResource(name, content, "text/plain")), nil 87 | }, 88 | ) 89 | if err != nil { 90 | panic(err) 91 | } 92 | } 93 | 94 | // Keep the server running 95 | select {} 96 | } 97 | -------------------------------------------------------------------------------- /examples/readme_server/readme_server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/metoro-io/mcp-golang" 6 | "github.com/metoro-io/mcp-golang/transport/stdio" 7 | ) 8 | 9 | // Tool arguments are just structs, annotated with jsonschema tags 10 | // More at https://mcpgolang.com/tools#schema-generation 11 | type Content struct { 12 | Title string `json:"title" jsonschema:"required,description=The title to submit"` 13 | Description *string `json:"description" jsonschema:"description=The description to submit"` 14 | } 15 | type MyFunctionsArguments struct { 16 | Submitter string `json:"submitter" jsonschema:"required,description=The name of the thing calling this tool (openai, google, claude, etc)"` 17 | Content Content `json:"content" jsonschema:"required,description=The content of the message"` 18 | } 19 | 20 | func main() { 21 | done := make(chan struct{}) 22 | 23 | server := mcp_golang.NewServer(stdio.NewStdioServerTransport()) 24 | err := server.RegisterTool("hello", "Say hello to a person", func(arguments MyFunctionsArguments) (*mcp_golang.ToolResponse, error) { 25 | return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Hello, %server!", arguments.Submitter))), nil 26 | }) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | err = server.RegisterPrompt("promt_test", "This is a test prompt", func(arguments Content) (*mcp_golang.PromptResponse, error) { 32 | return mcp_golang.NewPromptResponse("description", mcp_golang.NewPromptMessage(mcp_golang.NewTextContent(fmt.Sprintf("Hello, %server!", arguments.Title)), mcp_golang.RoleUser)), nil 33 | }) 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | err = server.RegisterResource("test://resource", "resource_test", "This is a test resource", "application/json", func() (*mcp_golang.ResourceResponse, error) { 39 | return mcp_golang.NewResourceResponse(mcp_golang.NewTextEmbeddedResource("test://resource", "This is a test resource", "application/json")), nil 40 | }) 41 | 42 | err = server.Serve() 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | <-done 48 | } 49 | -------------------------------------------------------------------------------- /examples/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | mcp "github.com/metoro-io/mcp-golang" 7 | "github.com/metoro-io/mcp-golang/transport/stdio" 8 | ) 9 | 10 | // HelloArgs represents the arguments for the hello tool 11 | type HelloArgs struct { 12 | Name string `json:"name" jsonschema:"required,description=The name to say hello to"` 13 | } 14 | 15 | func main() { 16 | // Create a transport for the server 17 | serverTransport := stdio.NewStdioServerTransport() 18 | 19 | // Create a new server with the transport 20 | server := mcp.NewServer(serverTransport) 21 | 22 | // Register a simple tool with the server 23 | err := server.RegisterTool("hello", "Says hello", func(args HelloArgs) (*mcp.ToolResponse, error) { 24 | message := fmt.Sprintf("Hello, %s!", args.Name) 25 | return mcp.NewToolResponse(mcp.NewTextContent(message)), nil 26 | }) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | // Start the server 32 | err = server.Serve() 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | // Keep the server running 38 | select {} 39 | } 40 | -------------------------------------------------------------------------------- /examples/simple_tool_docs/simple_tool_docs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | mcp_golang "github.com/metoro-io/mcp-golang" 6 | "github.com/metoro-io/mcp-golang/transport/stdio" 7 | ) 8 | 9 | type HelloArguments struct { 10 | Submitter string `json:"submitter" jsonschema:"required,description=The name of the thing calling this tool (openai or google or claude etc)'"` 11 | } 12 | 13 | // This is explained in the docs at https://mcpgolang.com/tools 14 | func main() { 15 | done := make(chan struct{}) 16 | server := mcp_golang.NewServer(stdio.NewStdioServerTransport()) 17 | err := server.RegisterTool("hello", "Say hello to a person", func(arguments HelloArguments) (*mcp_golang.ToolResponse, error) { 18 | return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Hello, %s!", arguments.Submitter))), nil 19 | }) 20 | err = server.Serve() 21 | if err != nil { 22 | panic(err) 23 | } 24 | <-done 25 | } 26 | -------------------------------------------------------------------------------- /examples/updating_registrations_on_the_fly/updating_registrations_on_the_fly.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | mcp_golang "github.com/metoro-io/mcp-golang" 6 | "github.com/metoro-io/mcp-golang/transport/stdio" 7 | "time" 8 | ) 9 | 10 | type HelloArguments struct { 11 | Submitter string `json:"submitter" jsonschema:"required,description=The name of the thing calling this tool (openai or google or claude etc)'"` 12 | } 13 | 14 | type Content struct { 15 | Title string `json:"title" jsonschema:"required,description=The title to submit"` 16 | Description *string `json:"description" jsonschema:"description=The description to submit"` 17 | } 18 | 19 | // This is a stupid server that demonstrates how to update registrations on the fly. 20 | // Every second the server will register a new tool, a new prompt and a new resource, then unregister the old ones. 21 | func main() { 22 | done := make(chan struct{}) 23 | server := mcp_golang.NewServer(stdio.NewStdioServerTransport()) 24 | err := server.Serve() 25 | if err != nil { 26 | panic(err) 27 | } 28 | go func() { 29 | for { 30 | err := server.RegisterTool("hello", "Say hello to a person", func(arguments HelloArguments) (*mcp_golang.ToolResponse, error) { 31 | return mcp_golang.NewToolResponse(mcp_golang.NewTextContent(fmt.Sprintf("Hello, %s!", arguments.Submitter))), nil 32 | }) 33 | if err != nil { 34 | panic(err) 35 | } 36 | time.Sleep(1 * time.Second) 37 | err = server.DeregisterTool("hello") 38 | if err != nil { 39 | panic(err) 40 | } 41 | } 42 | }() 43 | go func() { 44 | for { 45 | 46 | err = server.RegisterPrompt("prompt_test", "This is a test prompt", func(arguments Content) (*mcp_golang.PromptResponse, error) { 47 | return mcp_golang.NewPromptResponse("description", mcp_golang.NewPromptMessage(mcp_golang.NewTextContent(fmt.Sprintf("Hello, %server!", arguments.Title)), mcp_golang.RoleUser)), nil 48 | }) 49 | if err != nil { 50 | panic(err) 51 | } 52 | time.Sleep(1 * time.Second) 53 | err = server.DeregisterPrompt("prompt_test") 54 | if err != nil { 55 | panic(err) 56 | } 57 | } 58 | 59 | }() 60 | go func() { 61 | err = server.RegisterResource("test://resource", "resource_test", "This is a test resource", "application/json", func() (*mcp_golang.ResourceResponse, error) { 62 | return mcp_golang.NewResourceResponse(mcp_golang.NewTextEmbeddedResource("test://resource", "This is a test resource", "application/json")), nil 63 | }) 64 | if err != nil { 65 | panic(err) 66 | } 67 | time.Sleep(1 * time.Second) 68 | err = server.DeregisterResource("test://resource") 69 | if err != nil { 70 | panic(err) 71 | } 72 | }() 73 | 74 | <-done 75 | } 76 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/metoro-io/mcp-golang 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/invopop/jsonschema v0.12.0 7 | github.com/pkg/errors v0.9.1 8 | github.com/stretchr/testify v1.9.0 9 | github.com/tidwall/sjson v1.2.5 10 | ) 11 | 12 | require ( 13 | github.com/bahlo/generic-list-go v0.2.0 // indirect 14 | github.com/buger/jsonparser v1.1.1 // indirect 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/gin-contrib/sse v0.1.0 // indirect 17 | github.com/gin-gonic/gin v1.8.1 // indirect 18 | github.com/go-playground/locales v0.14.0 // indirect 19 | github.com/go-playground/universal-translator v0.18.0 // indirect 20 | github.com/go-playground/validator/v10 v10.10.0 // indirect 21 | github.com/goccy/go-json v0.9.7 // indirect 22 | github.com/json-iterator/go v1.1.12 // indirect 23 | github.com/leodido/go-urn v1.2.1 // indirect 24 | github.com/mailru/easyjson v0.7.7 // indirect 25 | github.com/mattn/go-isatty v0.0.14 // indirect 26 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 27 | github.com/modern-go/reflect2 v1.0.2 // indirect 28 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect 29 | github.com/pmezard/go-difflib v1.0.0 // indirect 30 | github.com/tidwall/gjson v1.18.0 // indirect 31 | github.com/tidwall/match v1.1.1 // indirect 32 | github.com/tidwall/pretty v1.2.1 // indirect 33 | github.com/ugorji/go/codec v1.2.7 // indirect 34 | github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect 35 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect 36 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect 37 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect 38 | golang.org/x/text v0.3.6 // indirect 39 | google.golang.org/protobuf v1.28.0 // indirect 40 | gopkg.in/yaml.v2 v2.4.0 // indirect 41 | gopkg.in/yaml.v3 v3.0.1 // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /internal/datastructures/generic_sync_map.go: -------------------------------------------------------------------------------- 1 | package datastructures 2 | 3 | import "sync" 4 | 5 | type SyncMap[K comparable, V any] struct { 6 | m sync.Map 7 | } 8 | 9 | func (m *SyncMap[K, V]) Delete(key K) { 10 | m.m.Delete(key) 11 | } 12 | func (m *SyncMap[K, V]) Load(key K) (value V, ok bool) { 13 | v, ok := m.m.Load(key) 14 | if !ok { 15 | return value, ok 16 | } 17 | return v.(V), ok 18 | } 19 | func (m *SyncMap[K, V]) LoadAndDelete(key K) (value V, loaded bool) { 20 | v, loaded := m.m.LoadAndDelete(key) 21 | if !loaded { 22 | return value, loaded 23 | } 24 | return v.(V), loaded 25 | } 26 | func (m *SyncMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { 27 | a, loaded := m.m.LoadOrStore(key, value) 28 | return a.(V), loaded 29 | } 30 | func (m *SyncMap[K, V]) Range(f func(key K, value V) bool) { 31 | m.m.Range(func(key, value any) bool { return f(key.(K), value.(V)) }) 32 | } 33 | func (m *SyncMap[K, V]) Store(key K, value V) { 34 | m.m.Store(key, value) 35 | } 36 | -------------------------------------------------------------------------------- /internal/protocol/types.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | type Result struct { 4 | // This result property is reserved by the protocol to allow clients and servers 5 | // to attach additional metadata to their responses. 6 | Meta ResultMeta `json:"_meta,omitempty" yaml:"_meta,omitempty" mapstructure:"_meta,omitempty"` 7 | 8 | AdditionalProperties interface{} `mapstructure:",remain"` 9 | } 10 | 11 | // This result property is reserved by the protocol to allow clients and servers to 12 | // attach additional metadata to their responses. 13 | type ResultMeta map[string]interface{} 14 | -------------------------------------------------------------------------------- /internal/schema/README.md: -------------------------------------------------------------------------------- 1 | # Schema Directory 2 | 3 | This directory contains the JSON schema for MCP types as taken from the [official MCP specification repo](https://github.com/modelcontextprotocol/specification). E.g. for the latest version, see [schema/mcp-schema-2024-10-07.json](schema/mcp-schema-2024-10-07.json). Taken from [here](https://github.com/modelcontextprotocol/specification/blob/bb5fdd282a4d0793822a569f573ebc36804d38f8/schema/schema.json). 4 | 5 | This schema is used to generate the types we use in this library and means that we adhere strictly to the spec. 6 | 7 | ## Generating types from a new schema 8 | 9 | We use the [go-jsonschema](https://github.com/atombender/go-jsonschema) library to generate types from the schema. To update the types, run `go generate ./...` in this directory. -------------------------------------------------------------------------------- /internal/schema/generate.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | //go:generate go run github.com/atombender/go-jsonschema@latest ./mcp-schema-2024-10-07.json -p schema -o ./types.go 4 | -------------------------------------------------------------------------------- /internal/testingutils/mock_transport.go: -------------------------------------------------------------------------------- 1 | package testingutils 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/metoro-io/mcp-golang/transport" 8 | ) 9 | 10 | // MockTransport implements Transport interface for testing 11 | type MockTransport struct { 12 | mu sync.RWMutex 13 | 14 | // Callbacks 15 | onClose func() 16 | onError func(error) 17 | onMessage func(ctx context.Context, message *transport.BaseJsonRpcMessage) 18 | 19 | // Test helpers 20 | messages []*transport.BaseJsonRpcMessage 21 | closed bool 22 | started bool 23 | } 24 | 25 | func NewMockTransport() *MockTransport { 26 | return &MockTransport{ 27 | messages: make([]*transport.BaseJsonRpcMessage, 0), 28 | } 29 | } 30 | 31 | func (t *MockTransport) Start(ctx context.Context) error { 32 | t.mu.Lock() 33 | t.started = true 34 | t.mu.Unlock() 35 | return nil 36 | } 37 | 38 | func (t *MockTransport) Send(ctx context.Context, message *transport.BaseJsonRpcMessage) error { 39 | t.mu.Lock() 40 | t.messages = append(t.messages, message) 41 | t.mu.Unlock() 42 | return nil 43 | } 44 | 45 | func (t *MockTransport) Close() error { 46 | t.mu.Lock() 47 | t.closed = true 48 | t.mu.Unlock() 49 | if t.onClose != nil { 50 | t.onClose() 51 | } 52 | return nil 53 | } 54 | 55 | func (t *MockTransport) SetCloseHandler(handler func()) { 56 | t.mu.Lock() 57 | t.onClose = handler 58 | t.mu.Unlock() 59 | } 60 | 61 | func (t *MockTransport) SetErrorHandler(handler func(error)) { 62 | t.mu.Lock() 63 | t.onError = handler 64 | t.mu.Unlock() 65 | } 66 | 67 | func (t *MockTransport) SetMessageHandler(handler func(ctx context.Context, message *transport.BaseJsonRpcMessage)) { 68 | t.mu.Lock() 69 | t.onMessage = handler 70 | t.mu.Unlock() 71 | } 72 | 73 | // Test helper methods 74 | 75 | func (t *MockTransport) SimulateMessage(msg *transport.BaseJsonRpcMessage) { 76 | t.mu.RLock() 77 | handler := t.onMessage 78 | t.mu.RUnlock() 79 | if handler != nil { 80 | handler(context.Background(), msg) 81 | } 82 | } 83 | 84 | func (t *MockTransport) SimulateError(err error) { 85 | t.mu.RLock() 86 | handler := t.onError 87 | t.mu.RUnlock() 88 | if handler != nil { 89 | handler(err) 90 | } 91 | } 92 | 93 | func (t *MockTransport) GetMessages() []*transport.BaseJsonRpcMessage { 94 | t.mu.RLock() 95 | defer t.mu.RUnlock() 96 | msgs := make([]*transport.BaseJsonRpcMessage, len(t.messages)) 97 | copy(msgs, t.messages) 98 | return msgs 99 | } 100 | 101 | func (t *MockTransport) IsClosed() bool { 102 | t.mu.RLock() 103 | defer t.mu.RUnlock() 104 | return t.closed 105 | } 106 | 107 | func (t *MockTransport) IsStarted() bool { 108 | t.mu.RLock() 109 | defer t.mu.RUnlock() 110 | return t.started 111 | } 112 | -------------------------------------------------------------------------------- /internal/tools/tool_types.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | // Definition for a tool the client can call. 4 | type ToolRetType struct { 5 | // A human-readable description of the tool. 6 | Description *string `json:"description,omitempty" yaml:"description,omitempty" mapstructure:"description,omitempty"` 7 | 8 | // A JSON Schema object defining the expected parameters for the tool. 9 | InputSchema interface{} `json:"inputSchema" yaml:"inputSchema" mapstructure:"inputSchema"` 10 | 11 | // The name of the tool. 12 | Name string `json:"name" yaml:"name" mapstructure:"name"` 13 | } 14 | type ToolsResponse struct { 15 | Tools []ToolRetType `json:"tools" yaml:"tools" mapstructure:"tools"` 16 | NextCursor *string `json:"nextCursor,omitempty" yaml:"nextCursor,omitempty" mapstructure:"nextCursor,omitempty"` 17 | } 18 | -------------------------------------------------------------------------------- /prompt_api.go: -------------------------------------------------------------------------------- 1 | package mcp_golang 2 | 3 | type PromptMessage struct { 4 | Content *Content `json:"content" yaml:"content" mapstructure:"content"` 5 | Role Role `json:"role" yaml:"role" mapstructure:"role"` 6 | } 7 | 8 | func NewPromptMessage(content *Content, role Role) *PromptMessage { 9 | return &PromptMessage{ 10 | Content: content, 11 | Role: role, 12 | } 13 | } 14 | 15 | // The server's response to a prompts/get request from the client. 16 | type PromptResponse struct { 17 | // An optional description for the prompt. 18 | Description *string `json:"description,omitempty" yaml:"description,omitempty" mapstructure:"description,omitempty"` 19 | 20 | // Messages corresponds to the JSON schema field "messages". 21 | Messages []*PromptMessage `json:"messages" yaml:"messages" mapstructure:"messages"` 22 | } 23 | 24 | func NewPromptResponse(description string, messages ...*PromptMessage) *PromptResponse { 25 | return &PromptResponse{ 26 | Description: &description, 27 | Messages: messages, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /prompt_response_types.go: -------------------------------------------------------------------------------- 1 | package mcp_golang 2 | 3 | import "encoding/json" 4 | 5 | type baseGetPromptRequestParamsArguments struct { 6 | // We will deserialize the arguments into the users struct later on 7 | Arguments json.RawMessage `json:"arguments,omitempty" yaml:"arguments,omitempty" mapstructure:"arguments,omitempty"` 8 | 9 | // The name of the prompt or prompt template. 10 | Name string `json:"name" yaml:"name" mapstructure:"name"` 11 | } 12 | 13 | // The server's response to a prompts/list request from the client. 14 | type ListPromptsResponse struct { 15 | // Prompts corresponds to the JSON schema field "prompts". 16 | Prompts []*PromptSchema `json:"prompts" yaml:"prompts" mapstructure:"prompts"` 17 | // NextCursor is a cursor for pagination. If not nil, there are more prompts available. 18 | NextCursor *string `json:"nextCursor,omitempty" yaml:"nextCursor,omitempty" mapstructure:"nextCursor,omitempty"` 19 | } 20 | 21 | // A PromptSchema or prompt template that the server offers. 22 | type PromptSchema struct { 23 | // A list of arguments to use for templating the prompt. 24 | Arguments []PromptSchemaArgument `json:"arguments,omitempty" yaml:"arguments,omitempty" mapstructure:"arguments,omitempty"` 25 | 26 | // An optional description of what this prompt provides 27 | Description *string `json:"description,omitempty" yaml:"description,omitempty" mapstructure:"description,omitempty"` 28 | 29 | // The name of the prompt or prompt template. 30 | Name string `json:"name" yaml:"name" mapstructure:"name"` 31 | } 32 | 33 | type PromptSchemaArgument struct { 34 | // A human-readable description of the argument. 35 | Description *string `json:"description,omitempty" yaml:"description,omitempty" mapstructure:"description,omitempty"` 36 | 37 | // The name of the argument. 38 | Name string `json:"name" yaml:"name" mapstructure:"name"` 39 | 40 | // Whether this argument must be provided. 41 | Required *bool `json:"required,omitempty" yaml:"required,omitempty" mapstructure:"required,omitempty"` 42 | } 43 | -------------------------------------------------------------------------------- /resource_api.go: -------------------------------------------------------------------------------- 1 | package mcp_golang 2 | 3 | // There are no arguments to the resource api 4 | 5 | type ResourceResponse struct { 6 | Contents []*EmbeddedResource `json:"contents"` 7 | } 8 | 9 | func NewResourceResponse(contents ...*EmbeddedResource) *ResourceResponse { 10 | return &ResourceResponse{ 11 | Contents: contents, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /resource_response_types.go: -------------------------------------------------------------------------------- 1 | package mcp_golang 2 | 3 | type readResourceRequestParams struct { 4 | // The URI of the resource to read. The URI can use any protocol; it is up to the 5 | // server how to interpret it. 6 | Uri string `json:"uri" yaml:"uri" mapstructure:"uri"` 7 | } 8 | 9 | // The server's response to a resources/list request from the client. 10 | type ListResourcesResponse struct { 11 | // Resources corresponds to the JSON schema field "resources". 12 | Resources []*ResourceSchema `json:"resources" yaml:"resources" mapstructure:"resources"` 13 | // NextCursor is a cursor for pagination. If not nil, there are more resources available. 14 | NextCursor *string `json:"nextCursor,omitempty" yaml:"nextCursor,omitempty" mapstructure:"nextCursor,omitempty"` 15 | } 16 | 17 | // A known resource that the server is capable of reading. 18 | type ResourceSchema struct { 19 | // Annotations corresponds to the JSON schema field "annotations". 20 | Annotations *Annotations `json:"annotations,omitempty" yaml:"annotations,omitempty" mapstructure:"annotations,omitempty"` 21 | 22 | // A description of what this resource represents. 23 | // 24 | // This can be used by clients to improve the LLM's understanding of available 25 | // resources. It can be thought of like a "hint" to the model. 26 | Description *string `json:"description,omitempty" yaml:"description,omitempty" mapstructure:"description,omitempty"` 27 | 28 | // The MIME type of this resource, if known. 29 | MimeType *string `json:"mimeType,omitempty" yaml:"mimeType,omitempty" mapstructure:"mimeType,omitempty"` 30 | 31 | // A human-readable name for this resource. 32 | // 33 | // This can be used by clients to populate UI elements. 34 | Name string `json:"name" yaml:"name" mapstructure:"name"` 35 | 36 | // The URI of this resource. 37 | Uri string `json:"uri" yaml:"uri" mapstructure:"uri"` 38 | } 39 | 40 | // A resource template that defines a pattern for dynamic resources. 41 | type ResourceTemplateSchema struct { 42 | // Annotations corresponds to the JSON schema field "annotations". 43 | Annotations *Annotations `json:"annotations,omitempty" yaml:"annotations,omitempty" mapstructure:"annotations,omitempty"` 44 | 45 | // A description of what resources matching this template represent. 46 | Description *string `json:"description,omitempty" yaml:"description,omitempty" mapstructure:"description,omitempty"` 47 | 48 | // The MIME type of resources matching this template, if known. 49 | MimeType *string `json:"mimeType,omitempty" yaml:"mimeType,omitempty" mapstructure:"mimeType,omitempty"` 50 | 51 | // A human-readable name for this template. 52 | Name string `json:"name" yaml:"name" mapstructure:"name"` 53 | 54 | // The URI template following RFC 6570. 55 | UriTemplate string `json:"uriTemplate" yaml:"uriTemplate" mapstructure:"uriTemplate"` 56 | } 57 | 58 | // The server's response to a resources/templates/list request from the client. 59 | type ListResourceTemplatesResponse struct { 60 | // Templates corresponds to the JSON schema field "templates". 61 | Templates []*ResourceTemplateSchema `json:"resourceTemplates" yaml:"resourceTemplates" mapstructure:"resourceTemplates"` 62 | // NextCursor is a cursor for pagination. If not nil, there are more templates available. 63 | NextCursor *string `json:"nextCursor,omitempty" yaml:"nextCursor,omitempty" mapstructure:"nextCursor,omitempty"` 64 | } 65 | -------------------------------------------------------------------------------- /resources/mcp-golang-logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metoro-io/mcp-golang/6598b3d737ebf5246e30df49aa2d0daf7f04fd17/resources/mcp-golang-logo.webp -------------------------------------------------------------------------------- /tool_api.go: -------------------------------------------------------------------------------- 1 | package mcp_golang 2 | 3 | // This is a union type of all the different ToolResponse that can be sent back to the client. 4 | // We allow creation through constructors only to make sure that the ToolResponse is valid. 5 | type ToolResponse struct { 6 | Content []*Content `json:"content" yaml:"content" mapstructure:"content"` 7 | } 8 | 9 | func NewToolResponse(content ...*Content) *ToolResponse { 10 | return &ToolResponse{ 11 | Content: content, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tool_response_types.go: -------------------------------------------------------------------------------- 1 | package mcp_golang 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // Capabilities that a server may support. Known capabilities are defined here, in 9 | // this schema, but this is not a closed set: any server can define its own, 10 | // additional capabilities. 11 | type ServerCapabilities struct { 12 | // Experimental, non-standard capabilities that the server supports. 13 | Experimental ServerCapabilitiesExperimental `json:"experimental,omitempty" yaml:"experimental,omitempty" mapstructure:"experimental,omitempty"` 14 | 15 | // Present if the server supports sending log messages to the client. 16 | Logging ServerCapabilitiesLogging `json:"logging,omitempty" yaml:"logging,omitempty" mapstructure:"logging,omitempty"` 17 | 18 | // Present if the server offers any prompt templates. 19 | Prompts *ServerCapabilitiesPrompts `json:"prompts,omitempty" yaml:"prompts,omitempty" mapstructure:"prompts,omitempty"` 20 | 21 | // Present if the server offers any resources to read. 22 | Resources *ServerCapabilitiesResources `json:"resources,omitempty" yaml:"resources,omitempty" mapstructure:"resources,omitempty"` 23 | 24 | // Present if the server offers any tools to call. 25 | Tools *ServerCapabilitiesTools `json:"tools,omitempty" yaml:"tools,omitempty" mapstructure:"tools,omitempty"` 26 | } 27 | 28 | // Experimental, non-standard capabilities that the server supports. 29 | type ServerCapabilitiesExperimental map[string]map[string]interface{} 30 | 31 | // Present if the server supports sending log messages to the client. 32 | type ServerCapabilitiesLogging map[string]interface{} 33 | 34 | // Present if the server offers any prompt templates. 35 | type ServerCapabilitiesPrompts struct { 36 | // Whether this server supports notifications for changes to the prompt list. 37 | ListChanged *bool `json:"listChanged,omitempty" yaml:"listChanged,omitempty" mapstructure:"listChanged,omitempty"` 38 | } 39 | 40 | // Present if the server offers any resources to read. 41 | type ServerCapabilitiesResources struct { 42 | // Whether this server supports notifications for changes to the resource list. 43 | ListChanged *bool `json:"listChanged,omitempty" yaml:"listChanged,omitempty" mapstructure:"listChanged,omitempty"` 44 | 45 | // Whether this server supports subscribing to resource updates. 46 | Subscribe *bool `json:"subscribe,omitempty" yaml:"subscribe,omitempty" mapstructure:"subscribe,omitempty"` 47 | } 48 | 49 | // Present if the server offers any tools to call. 50 | type ServerCapabilitiesTools struct { 51 | // Whether this server supports notifications for changes to the tool list. 52 | ListChanged *bool `json:"listChanged,omitempty" yaml:"listChanged,omitempty" mapstructure:"listChanged,omitempty"` 53 | } 54 | 55 | // After receiving an initialize request from the client, the server sends this 56 | // response. 57 | type InitializeResponse struct { 58 | // This result property is reserved by the protocol to allow clients and servers 59 | // to attach additional metadata to their responses. 60 | Meta initializeResultMeta `json:"_meta,omitempty" yaml:"_meta,omitempty" mapstructure:"_meta,omitempty"` 61 | 62 | // Capabilities corresponds to the JSON schema field "capabilities". 63 | Capabilities ServerCapabilities `json:"capabilities" yaml:"capabilities" mapstructure:"capabilities"` 64 | 65 | // Instructions describing how to use the server and its features. 66 | // 67 | // This can be used by clients to improve the LLM's understanding of available 68 | // tools, resources, etc. It can be thought of like a "hint" to the model. For 69 | // example, this information MAY be added to the system prompt. 70 | Instructions *string `json:"instructions,omitempty" yaml:"instructions,omitempty" mapstructure:"instructions,omitempty"` 71 | 72 | // The version of the Model Context Protocol that the server wants to use. This 73 | // may not match the version that the client requested. If the client cannot 74 | // support this version, it MUST disconnect. 75 | ProtocolVersion string `json:"protocolVersion" yaml:"protocolVersion" mapstructure:"protocolVersion"` 76 | 77 | // ServerInfo corresponds to the JSON schema field "serverInfo". 78 | ServerInfo implementation `json:"serverInfo" yaml:"serverInfo" mapstructure:"serverInfo"` 79 | } 80 | 81 | // This result property is reserved by the protocol to allow clients and servers to 82 | // attach additional metadata to their responses. 83 | type initializeResultMeta map[string]interface{} 84 | 85 | // UnmarshalJSON implements json.Unmarshaler. 86 | func (j *InitializeResponse) UnmarshalJSON(b []byte) error { 87 | var raw map[string]interface{} 88 | if err := json.Unmarshal(b, &raw); err != nil { 89 | return err 90 | } 91 | if _, ok := raw["capabilities"]; raw != nil && !ok { 92 | return fmt.Errorf("field capabilities in initializeResult: required") 93 | } 94 | if _, ok := raw["protocolVersion"]; raw != nil && !ok { 95 | return fmt.Errorf("field protocolVersion in initializeResult: required") 96 | } 97 | if _, ok := raw["serverInfo"]; raw != nil && !ok { 98 | return fmt.Errorf("field serverInfo in initializeResult: required") 99 | } 100 | type Plain InitializeResponse 101 | var plain Plain 102 | if err := json.Unmarshal(b, &plain); err != nil { 103 | return err 104 | } 105 | *j = InitializeResponse(plain) 106 | return nil 107 | } 108 | 109 | // Describes the name and version of an MCP implementation. 110 | type implementation struct { 111 | // Name corresponds to the JSON schema field "name". 112 | Name string `json:"name" yaml:"name" mapstructure:"name"` 113 | 114 | // Version corresponds to the JSON schema field "version". 115 | Version string `json:"version" yaml:"version" mapstructure:"version"` 116 | } 117 | 118 | // UnmarshalJSON implements json.Unmarshaler. 119 | func (j *implementation) UnmarshalJSON(b []byte) error { 120 | var raw map[string]interface{} 121 | if err := json.Unmarshal(b, &raw); err != nil { 122 | return err 123 | } 124 | if _, ok := raw["name"]; raw != nil && !ok { 125 | return fmt.Errorf("field name in implementation: required") 126 | } 127 | if _, ok := raw["version"]; raw != nil && !ok { 128 | return fmt.Errorf("field version in implementation: required") 129 | } 130 | type Plain implementation 131 | var plain Plain 132 | if err := json.Unmarshal(b, &plain); err != nil { 133 | return err 134 | } 135 | *j = implementation(plain) 136 | return nil 137 | } 138 | 139 | type baseCallToolRequestParams struct { 140 | // Arguments corresponds to the JSON schema field "arguments". 141 | // It is stored as a []byte to enable efficient marshaling and unmarshaling into custom types later on in the protocol 142 | Arguments json.RawMessage `json:"arguments" yaml:"arguments" mapstructure:"arguments"` 143 | 144 | // Name corresponds to the JSON schema field "name". 145 | Name string `json:"name" yaml:"name" mapstructure:"name"` 146 | } 147 | 148 | // Definition for a tool the client can call. 149 | type ToolRetType struct { 150 | // A human-readable description of the tool. 151 | Description *string `json:"description,omitempty" yaml:"description,omitempty" mapstructure:"description,omitempty"` 152 | 153 | // A JSON Schema object defining the expected parameters for the tool. 154 | InputSchema interface{} `json:"inputSchema" yaml:"inputSchema" mapstructure:"inputSchema"` 155 | 156 | // The name of the tool. 157 | Name string `json:"name" yaml:"name" mapstructure:"name"` 158 | } 159 | type ToolsResponse struct { 160 | Tools []ToolRetType `json:"tools" yaml:"tools" mapstructure:"tools"` 161 | NextCursor *string `json:"nextCursor,omitempty" yaml:"nextCursor,omitempty" mapstructure:"nextCursor,omitempty"` 162 | } 163 | -------------------------------------------------------------------------------- /transport/http/common.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "sync" 9 | 10 | "github.com/metoro-io/mcp-golang/transport" 11 | ) 12 | 13 | // baseTransport implements the common functionality for HTTP-based transports 14 | type baseTransport struct { 15 | messageHandler func(ctx context.Context, message *transport.BaseJsonRpcMessage) 16 | errorHandler func(error) 17 | closeHandler func() 18 | mu sync.RWMutex 19 | responseMap map[int64]chan *transport.BaseJsonRpcMessage 20 | } 21 | 22 | func newBaseTransport() *baseTransport { 23 | return &baseTransport{ 24 | responseMap: make(map[int64]chan *transport.BaseJsonRpcMessage), 25 | } 26 | } 27 | 28 | // Send implements Transport.Send 29 | func (t *baseTransport) Send(ctx context.Context, message *transport.BaseJsonRpcMessage) error { 30 | key := message.JsonRpcResponse.Id 31 | responseChannel := t.responseMap[int64(key)] 32 | if responseChannel == nil { 33 | return fmt.Errorf("no response channel found for key: %d", key) 34 | } 35 | responseChannel <- message 36 | return nil 37 | } 38 | 39 | // Close implements Transport.Close 40 | func (t *baseTransport) Close() error { 41 | if t.closeHandler != nil { 42 | t.closeHandler() 43 | } 44 | return nil 45 | } 46 | 47 | // SetCloseHandler implements Transport.SetCloseHandler 48 | func (t *baseTransport) SetCloseHandler(handler func()) { 49 | t.mu.Lock() 50 | defer t.mu.Unlock() 51 | t.closeHandler = handler 52 | } 53 | 54 | // SetErrorHandler implements Transport.SetErrorHandler 55 | func (t *baseTransport) SetErrorHandler(handler func(error)) { 56 | t.mu.Lock() 57 | defer t.mu.Unlock() 58 | t.errorHandler = handler 59 | } 60 | 61 | // SetMessageHandler implements Transport.SetMessageHandler 62 | func (t *baseTransport) SetMessageHandler(handler func(ctx context.Context, message *transport.BaseJsonRpcMessage)) { 63 | t.mu.Lock() 64 | defer t.mu.Unlock() 65 | t.messageHandler = handler 66 | } 67 | 68 | // handleMessage processes an incoming message and returns a response 69 | func (t *baseTransport) handleMessage(ctx context.Context, body []byte) (*transport.BaseJsonRpcMessage, error) { 70 | // Store the response writer for later use 71 | t.mu.Lock() 72 | var key int64 = 0 73 | 74 | for key < 1000000 { 75 | if _, ok := t.responseMap[key]; !ok { 76 | break 77 | } 78 | key = key + 1 79 | } 80 | t.responseMap[key] = make(chan *transport.BaseJsonRpcMessage) 81 | t.mu.Unlock() 82 | 83 | var prevId *transport.RequestId = nil 84 | deserialized := false 85 | // Try to unmarshal as a request first 86 | var request transport.BaseJSONRPCRequest 87 | if err := json.Unmarshal(body, &request); err == nil { 88 | deserialized = true 89 | id := request.Id 90 | prevId = &id 91 | request.Id = transport.RequestId(key) 92 | t.mu.RLock() 93 | handler := t.messageHandler 94 | t.mu.RUnlock() 95 | 96 | if handler != nil { 97 | handler(ctx, transport.NewBaseMessageRequest(&request)) 98 | } 99 | } 100 | 101 | // Try as a notification 102 | var notification transport.BaseJSONRPCNotification 103 | if !deserialized { 104 | if err := json.Unmarshal(body, ¬ification); err == nil { 105 | deserialized = true 106 | t.mu.RLock() 107 | handler := t.messageHandler 108 | t.mu.RUnlock() 109 | 110 | if handler != nil { 111 | handler(ctx, transport.NewBaseMessageNotification(¬ification)) 112 | } 113 | } 114 | } 115 | 116 | // Try as a response 117 | var response transport.BaseJSONRPCResponse 118 | if !deserialized { 119 | if err := json.Unmarshal(body, &response); err == nil { 120 | deserialized = true 121 | t.mu.RLock() 122 | handler := t.messageHandler 123 | t.mu.RUnlock() 124 | 125 | if handler != nil { 126 | handler(ctx, transport.NewBaseMessageResponse(&response)) 127 | } 128 | } 129 | } 130 | 131 | // Try as an error 132 | var errorResponse transport.BaseJSONRPCError 133 | if !deserialized { 134 | if err := json.Unmarshal(body, &errorResponse); err == nil { 135 | deserialized = true 136 | t.mu.RLock() 137 | handler := t.messageHandler 138 | t.mu.RUnlock() 139 | 140 | if handler != nil { 141 | handler(ctx, transport.NewBaseMessageError(&errorResponse)) 142 | } 143 | } 144 | } 145 | 146 | // Block until the response is received 147 | responseToUse := <-t.responseMap[key] 148 | delete(t.responseMap, key) 149 | if prevId != nil { 150 | responseToUse.JsonRpcResponse.Id = *prevId 151 | } 152 | 153 | return responseToUse, nil 154 | } 155 | 156 | // readBody reads and returns the body from an io.Reader 157 | func (t *baseTransport) readBody(reader io.Reader) ([]byte, error) { 158 | body, err := io.ReadAll(reader) 159 | if err != nil { 160 | if t.errorHandler != nil { 161 | t.errorHandler(fmt.Errorf("failed to read request body: %w", err)) 162 | } 163 | return nil, fmt.Errorf("failed to read request body: %w", err) 164 | } 165 | return body, nil 166 | } 167 | -------------------------------------------------------------------------------- /transport/http/gin.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/metoro-io/mcp-golang/transport" 11 | ) 12 | 13 | // GinTransport implements a stateless HTTP transport for MCP using Gin 14 | type GinTransport struct { 15 | *baseTransport 16 | } 17 | 18 | // NewGinTransport creates a new Gin transport 19 | func NewGinTransport() *GinTransport { 20 | return &GinTransport{ 21 | baseTransport: newBaseTransport(), 22 | } 23 | } 24 | 25 | // Start implements Transport.Start - no-op for Gin transport as it's handled by Gin 26 | func (t *GinTransport) Start(ctx context.Context) error { 27 | return nil 28 | } 29 | 30 | // Send implements Transport.Send 31 | func (t *GinTransport) Send(ctx context.Context, message *transport.BaseJsonRpcMessage) error { 32 | key := message.JsonRpcResponse.Id 33 | responseChannel := t.responseMap[int64(key)] 34 | if responseChannel == nil { 35 | return fmt.Errorf("no response channel found for key: %d", key) 36 | } 37 | responseChannel <- message 38 | return nil 39 | } 40 | 41 | // Close implements Transport.Close 42 | func (t *GinTransport) Close() error { 43 | if t.closeHandler != nil { 44 | t.closeHandler() 45 | } 46 | return nil 47 | } 48 | 49 | // SetCloseHandler implements Transport.SetCloseHandler 50 | func (t *GinTransport) SetCloseHandler(handler func()) { 51 | t.mu.Lock() 52 | defer t.mu.Unlock() 53 | t.closeHandler = handler 54 | } 55 | 56 | // SetErrorHandler implements Transport.SetErrorHandler 57 | func (t *GinTransport) SetErrorHandler(handler func(error)) { 58 | t.mu.Lock() 59 | defer t.mu.Unlock() 60 | t.errorHandler = handler 61 | } 62 | 63 | // SetMessageHandler implements Transport.SetMessageHandler 64 | func (t *GinTransport) SetMessageHandler(handler func(ctx context.Context, message *transport.BaseJsonRpcMessage)) { 65 | t.mu.Lock() 66 | defer t.mu.Unlock() 67 | t.messageHandler = handler 68 | } 69 | 70 | // Handler returns a Gin handler function that can be used with Gin's router 71 | func (t *GinTransport) Handler() gin.HandlerFunc { 72 | return func(c *gin.Context) { 73 | ctx := context.Background() 74 | ctx = context.WithValue(ctx, "ginContext", c) 75 | if c.Request.Method != http.MethodPost { 76 | c.String(http.StatusMethodNotAllowed, "Only POST method is supported") 77 | return 78 | } 79 | 80 | body, err := t.readBody(c.Request.Body) 81 | if err != nil { 82 | c.String(http.StatusBadRequest, err.Error()) 83 | return 84 | } 85 | 86 | response, err := t.handleMessage(ctx, body) 87 | if err != nil { 88 | c.String(http.StatusInternalServerError, err.Error()) 89 | return 90 | } 91 | 92 | jsonData, err := json.Marshal(response) 93 | if err != nil { 94 | if t.errorHandler != nil { 95 | t.errorHandler(fmt.Errorf("failed to marshal response: %w", err)) 96 | } 97 | c.String(http.StatusInternalServerError, "Failed to marshal response") 98 | return 99 | } 100 | 101 | c.Data(http.StatusOK, "application/json", jsonData) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /transport/http/http.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "sync" 9 | 10 | "github.com/metoro-io/mcp-golang/transport" 11 | ) 12 | 13 | // HTTPTransport implements a stateless HTTP transport for MCP 14 | type HTTPTransport struct { 15 | *baseTransport 16 | server *http.Server 17 | endpoint string 18 | messageHandler func(ctx context.Context, message *transport.BaseJsonRpcMessage) 19 | errorHandler func(error) 20 | closeHandler func() 21 | mu sync.RWMutex 22 | addr string 23 | } 24 | 25 | // NewHTTPTransport creates a new HTTP transport that listens on the specified endpoint 26 | func NewHTTPTransport(endpoint string) *HTTPTransport { 27 | return &HTTPTransport{ 28 | baseTransport: newBaseTransport(), 29 | endpoint: endpoint, 30 | addr: ":8080", // Default port 31 | } 32 | } 33 | 34 | // WithAddr sets the address to listen on 35 | func (t *HTTPTransport) WithAddr(addr string) *HTTPTransport { 36 | t.addr = addr 37 | return t 38 | } 39 | 40 | // Start implements Transport.Start 41 | func (t *HTTPTransport) Start(ctx context.Context) error { 42 | mux := http.NewServeMux() 43 | mux.HandleFunc(t.endpoint, t.handleRequest) 44 | 45 | t.server = &http.Server{ 46 | Addr: t.addr, 47 | Handler: mux, 48 | } 49 | 50 | return t.server.ListenAndServe() 51 | } 52 | 53 | // Send implements Transport.Send 54 | func (t *HTTPTransport) Send(ctx context.Context, message *transport.BaseJsonRpcMessage) error { 55 | key := message.JsonRpcResponse.Id 56 | fmt.Printf("[Send] Attempting to send response with key: %d\n", key) 57 | 58 | responseChannel := t.baseTransport.responseMap[int64(key)] 59 | if responseChannel == nil { 60 | fmt.Printf("[Send] Response map keys: %v\n", t.getResponseMapKeys()) 61 | 62 | return fmt.Errorf("no response channel found for key: %d", key) 63 | } 64 | responseChannel <- message 65 | return nil 66 | } 67 | 68 | // Helper method to get keys 69 | func (t *HTTPTransport) getResponseMapKeys() []int64 { 70 | keys := make([]int64, 0, len(t.baseTransport.responseMap)) 71 | for k := range t.baseTransport.responseMap { 72 | keys = append(keys, k) 73 | } 74 | return keys 75 | } 76 | 77 | // Close implements Transport.Close 78 | func (t *HTTPTransport) Close() error { 79 | if t.server != nil { 80 | if err := t.server.Close(); err != nil { 81 | return err 82 | } 83 | } 84 | if t.closeHandler != nil { 85 | t.closeHandler() 86 | } 87 | return nil 88 | } 89 | 90 | // SetCloseHandler implements Transport.SetCloseHandler 91 | func (t *HTTPTransport) SetCloseHandler(handler func()) { 92 | t.mu.Lock() 93 | defer t.mu.Unlock() 94 | t.closeHandler = handler 95 | } 96 | 97 | // SetErrorHandler implements Transport.SetErrorHandler 98 | func (t *HTTPTransport) SetErrorHandler(handler func(error)) { 99 | t.mu.Lock() 100 | defer t.mu.Unlock() 101 | t.errorHandler = handler 102 | } 103 | 104 | // SetMessageHandler implements Transport.SetMessageHandler 105 | func (t *HTTPTransport) SetMessageHandler(handler func(ctx context.Context, message *transport.BaseJsonRpcMessage)) { 106 | t.mu.Lock() 107 | defer t.mu.Unlock() 108 | t.baseTransport.messageHandler = handler 109 | t.messageHandler = handler 110 | } 111 | 112 | func (t *HTTPTransport) handleRequest(w http.ResponseWriter, r *http.Request) { 113 | if r.Method != http.MethodPost { 114 | http.Error(w, "Only POST method is supported", http.StatusMethodNotAllowed) 115 | return 116 | } 117 | 118 | ctx := r.Context() 119 | body, err := t.readBody(r.Body) 120 | if err != nil { 121 | http.Error(w, err.Error(), http.StatusBadRequest) 122 | return 123 | } 124 | 125 | response, err := t.handleMessage(ctx, body) 126 | if err != nil { 127 | http.Error(w, err.Error(), http.StatusInternalServerError) 128 | return 129 | } 130 | 131 | jsonData, err := json.Marshal(response) 132 | if err != nil { 133 | if t.errorHandler != nil { 134 | t.errorHandler(fmt.Errorf("failed to marshal response: %w", err)) 135 | } 136 | http.Error(w, "Failed to marshal response", http.StatusInternalServerError) 137 | return 138 | } 139 | 140 | w.Header().Set("Content-Type", "application/json") 141 | w.Write(jsonData) 142 | } 143 | -------------------------------------------------------------------------------- /transport/http/http_client.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "sync" 11 | 12 | "github.com/metoro-io/mcp-golang/transport" 13 | ) 14 | 15 | type HTTPClient interface { 16 | Do(r *http.Request) (*http.Response, error) 17 | } 18 | 19 | // HTTPClientTransport implements a client-side HTTP transport for MCP 20 | type HTTPClientTransport struct { 21 | baseURL string 22 | endpoint string 23 | messageHandler func(ctx context.Context, message *transport.BaseJsonRpcMessage) 24 | errorHandler func(error) 25 | closeHandler func() 26 | mu sync.RWMutex 27 | client HTTPClient 28 | headers map[string]string 29 | } 30 | 31 | // NewHTTPClientTransport creates a new HTTP client transport that connects to the specified endpoint 32 | func NewHTTPClientTransport(endpoint string) *HTTPClientTransport { 33 | return &HTTPClientTransport{ 34 | endpoint: endpoint, 35 | client: &http.Client{}, 36 | headers: make(map[string]string), 37 | } 38 | } 39 | 40 | // WithClient allows to set a custom HTTP client 41 | func (t *HTTPClientTransport) WithClient(c HTTPClient) *HTTPClientTransport { 42 | t.client = c 43 | return t 44 | } 45 | 46 | // WithBaseURL sets the base URL to connect to 47 | func (t *HTTPClientTransport) WithBaseURL(baseURL string) *HTTPClientTransport { 48 | t.baseURL = baseURL 49 | return t 50 | } 51 | 52 | // WithHeader adds a header to the request 53 | func (t *HTTPClientTransport) WithHeader(key, value string) *HTTPClientTransport { 54 | t.headers[key] = value 55 | return t 56 | } 57 | 58 | // Start implements Transport.Start 59 | func (t *HTTPClientTransport) Start(ctx context.Context) error { 60 | // Does nothing in the stateless http client transport 61 | return nil 62 | } 63 | 64 | // Send implements Transport.Send 65 | func (t *HTTPClientTransport) Send(ctx context.Context, message *transport.BaseJsonRpcMessage) error { 66 | jsonData, err := json.Marshal(message) 67 | if err != nil { 68 | return fmt.Errorf("failed to marshal message: %w", err) 69 | } 70 | 71 | url := fmt.Sprintf("%s%s", t.baseURL, t.endpoint) 72 | req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonData)) 73 | if err != nil { 74 | return fmt.Errorf("failed to create request: %w", err) 75 | } 76 | req.Header.Set("Content-Type", "application/json") 77 | for key, value := range t.headers { 78 | req.Header.Set(key, value) 79 | } 80 | 81 | resp, err := t.client.Do(req) 82 | if err != nil { 83 | return fmt.Errorf("failed to send request: %w", err) 84 | } 85 | defer resp.Body.Close() 86 | 87 | body, err := io.ReadAll(resp.Body) 88 | if err != nil { 89 | return fmt.Errorf("failed to read response: %w", err) 90 | } 91 | 92 | if resp.StatusCode != http.StatusOK { 93 | return fmt.Errorf("server returned error: %s (status: %d)", string(body), resp.StatusCode) 94 | } 95 | 96 | if len(body) > 0 { 97 | // Try to unmarshal as a response first 98 | var response transport.BaseJSONRPCResponse 99 | if err := json.Unmarshal(body, &response); err == nil { 100 | t.mu.RLock() 101 | handler := t.messageHandler 102 | t.mu.RUnlock() 103 | 104 | if handler != nil { 105 | handler(ctx, transport.NewBaseMessageResponse(&response)) 106 | } 107 | return nil 108 | } 109 | 110 | // Try as an error 111 | var errorResponse transport.BaseJSONRPCError 112 | if err := json.Unmarshal(body, &errorResponse); err == nil { 113 | t.mu.RLock() 114 | handler := t.messageHandler 115 | t.mu.RUnlock() 116 | 117 | if handler != nil { 118 | handler(ctx, transport.NewBaseMessageError(&errorResponse)) 119 | } 120 | return nil 121 | } 122 | 123 | // Try as a notification 124 | var notification transport.BaseJSONRPCNotification 125 | if err := json.Unmarshal(body, ¬ification); err == nil { 126 | t.mu.RLock() 127 | handler := t.messageHandler 128 | t.mu.RUnlock() 129 | 130 | if handler != nil { 131 | handler(ctx, transport.NewBaseMessageNotification(¬ification)) 132 | } 133 | return nil 134 | } 135 | 136 | // Try as a request 137 | var request transport.BaseJSONRPCRequest 138 | if err := json.Unmarshal(body, &request); err == nil { 139 | t.mu.RLock() 140 | handler := t.messageHandler 141 | t.mu.RUnlock() 142 | 143 | if handler != nil { 144 | handler(ctx, transport.NewBaseMessageRequest(&request)) 145 | } 146 | return nil 147 | } 148 | 149 | return fmt.Errorf("received invalid response: %s", string(body)) 150 | } 151 | 152 | return nil 153 | } 154 | 155 | // Close implements Transport.Close 156 | func (t *HTTPClientTransport) Close() error { 157 | if t.closeHandler != nil { 158 | t.closeHandler() 159 | } 160 | return nil 161 | } 162 | 163 | // SetCloseHandler implements Transport.SetCloseHandler 164 | func (t *HTTPClientTransport) SetCloseHandler(handler func()) { 165 | t.mu.Lock() 166 | defer t.mu.Unlock() 167 | t.closeHandler = handler 168 | } 169 | 170 | // SetErrorHandler implements Transport.SetErrorHandler 171 | func (t *HTTPClientTransport) SetErrorHandler(handler func(error)) { 172 | t.mu.Lock() 173 | defer t.mu.Unlock() 174 | t.errorHandler = handler 175 | } 176 | 177 | // SetMessageHandler implements Transport.SetMessageHandler 178 | func (t *HTTPClientTransport) SetMessageHandler(handler func(ctx context.Context, message *transport.BaseJsonRpcMessage)) { 179 | t.mu.Lock() 180 | defer t.mu.Unlock() 181 | t.messageHandler = handler 182 | } 183 | -------------------------------------------------------------------------------- /transport/sse/internal/sse/sse.go: -------------------------------------------------------------------------------- 1 | // /* 2 | // Package mcp implements Server-Sent Events (SSE) transport for JSON-RPC communication. 3 | // 4 | // SSE Transport Overview: 5 | // This implementation provides a bidirectional communication channel between client and server: 6 | // - Server to Client: Uses Server-Sent Events (SSE) for real-time message streaming 7 | // - Client to Server: Uses HTTP POST requests for sending messages 8 | // 9 | // Key Features: 10 | // 1. Bidirectional Communication: 11 | // - SSE for server-to-client streaming (one-way, real-time updates) 12 | // - HTTP POST endpoints for client-to-server messages 13 | // 14 | // 2. Session Management: 15 | // - Unique session IDs for each connection 16 | // - Proper connection lifecycle management 17 | // - Automatic cleanup on connection close 18 | // 19 | // 3. Message Handling: 20 | // - JSON-RPC message format support 21 | // - Automatic message type detection (request vs response) 22 | // - Built-in error handling and reporting 23 | // - Message size limits for security 24 | // 25 | // 4. Security Features: 26 | // - Content-type validation 27 | // - Message size limits (4MB default) 28 | // - Error handling for malformed messages 29 | // 30 | // Usage Example: 31 | // 32 | // // Create a new SSE transport 33 | // transport, err := NewSSETransport("/messages", responseWriter) 34 | // if err != nil { 35 | // log.Fatal(err) 36 | // } 37 | // 38 | // // Set up message handling 39 | // transport.SetMessageHandler(func(msg JSONRPCMessage) { 40 | // // Handle incoming messages 41 | // }) 42 | // 43 | // // Start the SSE connection 44 | // if err := transport.Start(context.Background()); err != nil { 45 | // log.Fatal(err) 46 | // } 47 | // 48 | // // Send a message 49 | // msg := JSONRPCResponse{ 50 | // Jsonrpc: "2.0", 51 | // Result: Result{...}, 52 | // Id: 1, 53 | // } 54 | // if err := transport.Send(msg); err != nil { 55 | // log.Fatal(err) 56 | // } 57 | // 58 | // */ 59 | package sse 60 | 61 | // 62 | //import ( 63 | // "context" 64 | // "encoding/json" 65 | // "fmt" 66 | // "github.com/metoro-io/mcp-golang/transport" 67 | // "net/http" 68 | // "sync" 69 | // 70 | // "github.com/google/uuid" 71 | //) 72 | // 73 | //const ( 74 | // maxMessageSize = 4 * 1024 * 1024 // 4MB 75 | //) 76 | // 77 | //// SSETransport implements a Server-Sent Events transport for JSON-RPC messages 78 | //type SSETransport struct { 79 | // endpoint string 80 | // sessionID string 81 | // writer http.ResponseWriter 82 | // flusher http.Flusher 83 | // mu sync.Mutex 84 | // isConnected bool 85 | // 86 | // // Callbacks 87 | // closeHandler func() 88 | // errorHandler func(error) 89 | // messageHandler func(message *transport.BaseJsonRpcMessage) 90 | //} 91 | // 92 | //// NewSSETransport creates a new SSE transport with the given endpoint and response writer 93 | //func NewSSETransport(endpoint string, w http.ResponseWriter) (*SSETransport, error) { 94 | // flusher, ok := w.(http.Flusher) 95 | // if !ok { 96 | // return nil, fmt.Errorf("streaming not supported") 97 | // } 98 | // 99 | // return &SSETransport{ 100 | // endpoint: endpoint, 101 | // sessionID: uuid.New().String(), 102 | // writer: w, 103 | // flusher: flusher, 104 | // }, nil 105 | //} 106 | // 107 | //// Start initializes the SSE connection 108 | //func (t *SSETransport) Start(ctx context.Context) error { 109 | // t.mu.Lock() 110 | // defer t.mu.Unlock() 111 | // 112 | // if t.isConnected { 113 | // return fmt.Errorf("SSE transport already started") 114 | // } 115 | // 116 | // // Set SSE headers 117 | // h := t.writer.Header() 118 | // h.Set("Content-Type", "text/event-stream") 119 | // h.Set("Cache-Control", "no-cache") 120 | // h.Set("Connection", "keep-alive") 121 | // h.Set("Access-Control-Allow-Origin", "*") 122 | // 123 | // // Send the endpoint event 124 | // endpointURL := fmt.Sprintf("%s?sessionId=%s", t.endpoint, t.sessionID) 125 | // if err := t.writeEvent("endpoint", endpointURL); err != nil { 126 | // return err 127 | // } 128 | // 129 | // t.isConnected = true 130 | // 131 | // // Handle context cancellation 132 | // go func() { 133 | // <-ctx.Done() 134 | // t.Close() 135 | // }() 136 | // 137 | // return nil 138 | //} 139 | // 140 | //// HandleMessage processes an incoming message 141 | //func (t *SSETransport) HandleMessage(msg []byte) error { 142 | // var rpcMsg map[string]interface{} 143 | // if err := json.Unmarshal(msg, &rpcMsg); err != nil { 144 | // if t.errorHandler != nil { 145 | // t.errorHandler(err) 146 | // } 147 | // return err 148 | // } 149 | // 150 | // // Parse as a JSONRPCMessage 151 | // var jsonrpcMsg JSONRPCMessage 152 | // if _, ok := rpcMsg["method"]; ok { 153 | // var req JSONRPCRequest 154 | // if err := json.Unmarshal(msg, &req); err != nil { 155 | // if t.errorHandler != nil { 156 | // t.errorHandler(err) 157 | // } 158 | // return err 159 | // } 160 | // jsonrpcMsg = &req 161 | // } else { 162 | // var resp JSONRPCResponse 163 | // if err := json.Unmarshal(msg, &resp); err != nil { 164 | // if t.errorHandler != nil { 165 | // t.errorHandler(err) 166 | // } 167 | // return err 168 | // } 169 | // jsonrpcMsg = &resp 170 | // } 171 | // 172 | // if t.messageHandler != nil { 173 | // t.messageHandler(jsonrpcMsg) 174 | // } 175 | // return nil 176 | //} 177 | // 178 | //// Send sends a message over the SSE connection 179 | //func (t *SSETransport) Send(msg JSONRPCMessage) error { 180 | // t.mu.Lock() 181 | // defer t.mu.Unlock() 182 | // 183 | // if !t.isConnected { 184 | // return fmt.Errorf("not connected") 185 | // } 186 | // 187 | // data, err := json.Marshal(msg) 188 | // if err != nil { 189 | // return err 190 | // } 191 | // 192 | // return t.writeEvent("message", string(data)) 193 | //} 194 | // 195 | //// Close closes the SSE connection 196 | //func (t *SSETransport) Close() error { 197 | // t.mu.Lock() 198 | // defer t.mu.Unlock() 199 | // 200 | // if !t.isConnected { 201 | // return nil 202 | // } 203 | // 204 | // t.isConnected = false 205 | // if t.closeHandler != nil { 206 | // t.closeHandler() 207 | // } 208 | // return nil 209 | //} 210 | // 211 | //// SetCloseHandler sets the callback for when the connection is closed 212 | //func (t *SSETransport) SetCloseHandler(handler func()) { 213 | // t.mu.Lock() 214 | // defer t.mu.Unlock() 215 | // t.closeHandler = handler 216 | //} 217 | // 218 | //// SetErrorHandler sets the callback for when an error occurs 219 | //func (t *SSETransport) SetErrorHandler(handler func(error)) { 220 | // t.mu.Lock() 221 | // defer t.mu.Unlock() 222 | // t.errorHandler = handler 223 | //} 224 | // 225 | //// SetMessageHandler sets the callback for when a message is received 226 | //func (t *SSETransport) SetMessageHandler(handler func(JSONRPCMessage)) { 227 | // t.mu.Lock() 228 | // defer t.mu.Unlock() 229 | // t.messageHandler = handler 230 | //} 231 | // 232 | //// SessionID returns the unique session identifier for this transport 233 | //func (t *SSETransport) SessionID() string { 234 | // return t.sessionID 235 | //} 236 | // 237 | //// writeEvent writes an SSE event with the given event type and data 238 | //func (t *SSETransport) writeEvent(event, data string) error { 239 | // if _, err := fmt.Fprintf(t.writer, "event: %s\ndata: %s\n\n", event, data); err != nil { 240 | // return err 241 | // } 242 | // t.flusher.Flush() 243 | // return nil 244 | //} 245 | -------------------------------------------------------------------------------- /transport/sse/sse_server.go: -------------------------------------------------------------------------------- 1 | package sse 2 | 3 | // 4 | //import ( 5 | // "context" 6 | // "fmt" 7 | // sse2 "github.com/metoro-io/mcp-golang/transport/sse/internal/sse" 8 | // "io" 9 | // "net/http" 10 | //) 11 | // 12 | //// SSEServerTransport implements a server-side SSE transport 13 | //type SSEServerTransport struct { 14 | // transport *sse2.SSETransport 15 | //} 16 | // 17 | //// NewSSEServerTransport creates a new SSE server transport 18 | //func NewSSEServerTransport(endpoint string, w http.ResponseWriter) (*SSEServerTransport, error) { 19 | // transport, err := sse2.NewSSETransport(endpoint, w) 20 | // if err != nil { 21 | // return nil, err 22 | // } 23 | // 24 | // return &SSEServerTransport{ 25 | // transport: transport, 26 | // }, nil 27 | //} 28 | // 29 | //// Start initializes the SSE connection 30 | //func (s *SSEServerTransport) Start(ctx context.Context) error { 31 | // return s.transport.Start(ctx) 32 | //} 33 | // 34 | //// HandlePostMessage processes an incoming POST request containing a JSON-RPC message 35 | //func (s *SSEServerTransport) HandlePostMessage(r *http.Request) error { 36 | // if r.Method != http.MethodPost { 37 | // return fmt.Errorf("method not allowed: %s", r.Method) 38 | // } 39 | // 40 | // contentType := r.Header.Get("Content-Type") 41 | // if contentType != "application/json" { 42 | // return fmt.Errorf("unsupported Content type: %s", contentType) 43 | // } 44 | // 45 | // body, err := io.ReadAll(io.LimitReader(r.Body, sse2.maxMessageSize)) 46 | // if err != nil { 47 | // return fmt.Errorf("failed to read request body: %w", err) 48 | // } 49 | // defer r.Body.Close() 50 | // 51 | // return s.transport.HandleMessage(body) 52 | //} 53 | // 54 | //// Send sends a message over the SSE connection 55 | //func (s *SSEServerTransport) Send(msg JSONRPCMessage) error { 56 | // return s.transport.Send(msg) 57 | //} 58 | // 59 | //// Close closes the SSE connection 60 | //func (s *SSEServerTransport) Close() error { 61 | // return s.transport.Close() 62 | //} 63 | // 64 | //// SetCloseHandler sets the callback for when the connection is closed 65 | //func (s *SSEServerTransport) SetCloseHandler(handler func()) { 66 | // s.transport.SetCloseHandler(handler) 67 | //} 68 | // 69 | //// SetErrorHandler sets the callback for when an error occurs 70 | //func (s *SSEServerTransport) SetErrorHandler(handler func(error)) { 71 | // s.transport.SetErrorHandler(handler) 72 | //} 73 | // 74 | //// SetMessageHandler sets the callback for when a message is received 75 | //func (s *SSEServerTransport) SetMessageHandler(handler func(JSONRPCMessage)) { 76 | // s.transport.SetMessageHandler(handler) 77 | //} 78 | // 79 | //// SessionID returns the unique session identifier for this transport 80 | //func (s *SSEServerTransport) SessionID() string { 81 | // return s.transport.SessionID() 82 | //} 83 | -------------------------------------------------------------------------------- /transport/sse/sse_server_test.go: -------------------------------------------------------------------------------- 1 | package sse 2 | 3 | // 4 | //import ( 5 | // "bytes" 6 | // "context" 7 | // "encoding/json" 8 | // "github.com/metoro-io/mcp-golang" 9 | // "net/http" 10 | // "net/http/httptest" 11 | // "strings" 12 | // "testing" 13 | // 14 | // "github.com/stretchr/testify/assert" 15 | //) 16 | // 17 | //func TestSSEServerTransport(t *testing.T) { 18 | // t.Run("basic message handling", func(t *testing.T) { 19 | // w := httptest.NewRecorder() 20 | // transport, err := NewSSEServerTransport("/messages", w) 21 | // assert.NoError(t, err) 22 | // 23 | // var receivedMsg JSONRPCMessage 24 | // transport.SetMessageHandler(func(msg JSONRPCMessage) { 25 | // receivedMsg = msg 26 | // }) 27 | // 28 | // ctx := context.Background() 29 | // err = transport.Start(ctx) 30 | // assert.NoError(t, err) 31 | // 32 | // // Verify SSE headers 33 | // headers := w.Header() 34 | // assert.Equal(t, "text/event-stream", headers.Get("Content-Type")) 35 | // assert.Equal(t, "no-cache", headers.Get("Cache-Control")) 36 | // assert.Equal(t, "keep-alive", headers.Get("Connection")) 37 | // 38 | // // Verify endpoint event was sent 39 | // body := w.Body.String() 40 | // assert.Contains(t, body, "event: endpoint") 41 | // assert.Contains(t, body, "/messages?sessionId=") 42 | // 43 | // // Test message handling 44 | // msg := JSONRPCRequest{ 45 | // Jsonrpc: "2.0", 46 | // Method: "test", 47 | // Id: 1, 48 | // } 49 | // msgBytes, err := json.Marshal(msg) 50 | // assert.NoError(t, err) 51 | // 52 | // httpReq := httptest.NewRequest(http.MethodPost, "/messages", bytes.NewReader(msgBytes)) 53 | // httpReq.Header.Set("Content-Type", "application/json") 54 | // err = transport.HandlePostMessage(httpReq) 55 | // assert.NoError(t, err) 56 | // 57 | // // Verify received message 58 | // rpcReq, ok := receivedMsg.(*JSONRPCRequest) 59 | // assert.True(t, ok) 60 | // assert.Equal(t, "2.0", rpcReq.Jsonrpc) 61 | // assert.Equal(t, mcp.RequestId(1), rpcReq.Id) 62 | // 63 | // err = transport.Close() 64 | // assert.NoError(t, err) 65 | // }) 66 | // 67 | // t.Run("send message", func(t *testing.T) { 68 | // w := httptest.NewRecorder() 69 | // transport, err := NewSSEServerTransport("/messages", w) 70 | // assert.NoError(t, err) 71 | // 72 | // ctx := context.Background() 73 | // err = transport.Start(ctx) 74 | // assert.NoError(t, err) 75 | // 76 | // msg := JSONRPCResponse{ 77 | // Jsonrpc: "2.0", 78 | // Result: Result{AdditionalProperties: map[string]interface{}{"status": "ok"}}, 79 | // Id: 1, 80 | // } 81 | // 82 | // err = transport.Send(msg) 83 | // assert.NoError(t, err) 84 | // 85 | // // Verify output contains the message 86 | // body := w.Body.String() 87 | // assert.Contains(t, body, `event: message`) 88 | // assert.Contains(t, body, `"result":{"AdditionalProperties":{"status":"ok"}}`) 89 | // }) 90 | // 91 | // t.Run("error handling", func(t *testing.T) { 92 | // w := httptest.NewRecorder() 93 | // transport, err := NewSSEServerTransport("/messages", w) 94 | // assert.NoError(t, err) 95 | // 96 | // var receivedErr error 97 | // transport.SetErrorHandler(func(err error) { 98 | // receivedErr = err 99 | // }) 100 | // 101 | // ctx := context.Background() 102 | // err = transport.Start(ctx) 103 | // assert.NoError(t, err) 104 | // 105 | // // Test invalid JSON 106 | // req := httptest.NewRequest(http.MethodPost, "/messages", strings.NewReader("invalid json")) 107 | // req.Header.Set("Content-Type", "application/json") 108 | // err = transport.HandlePostMessage(req) 109 | // assert.Error(t, err) 110 | // assert.NotNil(t, receivedErr) 111 | // assert.Contains(t, receivedErr.Error(), "invalid") 112 | // 113 | // // Test invalid Content type 114 | // req = httptest.NewRequest(http.MethodPost, "/messages", strings.NewReader("{}")) 115 | // req.Header.Set("Content-Type", "text/plain") 116 | // err = transport.HandlePostMessage(req) 117 | // assert.Error(t, err) 118 | // assert.Contains(t, err.Error(), "unsupported Content type") 119 | // 120 | // // Test invalid method 121 | // req = httptest.NewRequest(http.MethodGet, "/messages", nil) 122 | // err = transport.HandlePostMessage(req) 123 | // assert.Error(t, err) 124 | // assert.Contains(t, err.Error(), "method not allowed") 125 | // }) 126 | //} 127 | -------------------------------------------------------------------------------- /transport/stdio/internal/stdio/stdio.go: -------------------------------------------------------------------------------- 1 | // This file implements the stdio transport layer for JSON-RPC communication. 2 | // It provides functionality to read and write JSON-RPC messages over standard input/output 3 | // streams, similar to the TypeScript implementation in @typescript-sdk/src/shared/stdio.ts. 4 | // 5 | // Key Components: 6 | // 7 | // 1. ReadBuffer: 8 | // - Buffers continuous stdio stream into discrete JSON-RPC messages 9 | // - Thread-safe with mutex protection 10 | // - Handles message framing using newline delimiters 11 | // - Methods: Append (add data), ReadMessage (read complete message), Clear (reset buffer) 12 | // 13 | // 2. StdioTransport: 14 | // - Implements the Transport interface using stdio 15 | // - Uses bufio.Reader for efficient buffered reading 16 | // - Thread-safe with mutex protection 17 | // - Supports: 18 | // - Asynchronous message reading 19 | // - Message sending with newline framing 20 | // - Proper cleanup on close 21 | // - Event handlers for close, error, and message events 22 | // 23 | // 3. Message Handling: 24 | // - Deserializes JSON-RPC messages into appropriate types: 25 | // - JSONRPCRequest: Messages with ID and method 26 | // - JSONRPCNotification: Messages with method but no ID 27 | // - JSONRPCError: Error responses with ID 28 | // - Generic responses: Success responses with ID 29 | // - Serializes messages to JSON with newline termination 30 | // 31 | // Thread Safety: 32 | // - All public methods are thread-safe 33 | // - Uses sync.Mutex for state protection 34 | // - Safe for concurrent reading and writing 35 | // 36 | // Usage: 37 | // 38 | // transport := NewStdioTransport() 39 | // transport.SetMessageHandler(func(msg interface{}) { 40 | // // Handle incoming message 41 | // }) 42 | // transport.Start() 43 | // defer transport.Close() 44 | // 45 | // // Send a message 46 | // transport.Send(map[string]interface{}{ 47 | // "jsonrpc": "2.0", 48 | // "method": "test", 49 | // "params": map[string]interface{}{}, 50 | // }) 51 | // 52 | // Error Handling: 53 | // - All methods return meaningful errors 54 | // - Transport supports error handler for async errors 55 | // - Proper cleanup on error conditions 56 | // 57 | // For more details, see the test file stdio_test.go. 58 | package stdio 59 | 60 | import ( 61 | "encoding/json" 62 | "errors" 63 | "github.com/metoro-io/mcp-golang/transport" 64 | "sync" 65 | ) 66 | 67 | // ReadBuffer buffers a continuous stdio stream into discrete JSON-RPC messages. 68 | type ReadBuffer struct { 69 | mu sync.Mutex 70 | buffer []byte 71 | } 72 | 73 | // NewReadBuffer creates a new ReadBuffer. 74 | func NewReadBuffer() *ReadBuffer { 75 | return &ReadBuffer{} 76 | } 77 | 78 | // Append adds a chunk of data to the buffer. 79 | func (rb *ReadBuffer) Append(chunk []byte) { 80 | rb.mu.Lock() 81 | defer rb.mu.Unlock() 82 | 83 | if rb.buffer == nil { 84 | rb.buffer = chunk 85 | } else { 86 | rb.buffer = append(rb.buffer, chunk...) 87 | } 88 | } 89 | 90 | // ReadMessage reads a complete JSON-RPC message from the buffer. 91 | // Returns nil if no complete message is available. 92 | func (rb *ReadBuffer) ReadMessage() (*transport.BaseJsonRpcMessage, error) { 93 | rb.mu.Lock() 94 | defer rb.mu.Unlock() 95 | 96 | if rb.buffer == nil { 97 | return nil, nil 98 | } 99 | 100 | // Find newline 101 | for i := 0; i < len(rb.buffer); i++ { 102 | if rb.buffer[i] == '\n' { 103 | // Extract line 104 | line := string(rb.buffer[:i]) 105 | //println("read line: ", line) 106 | rb.buffer = rb.buffer[i+1:] 107 | return deserializeMessage(line) 108 | } 109 | } 110 | 111 | return nil, nil 112 | } 113 | 114 | // Clear clears the buffer. 115 | func (rb *ReadBuffer) Clear() { 116 | rb.mu.Lock() 117 | defer rb.mu.Unlock() 118 | rb.buffer = nil 119 | } 120 | 121 | // deserializeMessage deserializes a JSON-RPC message from a string. 122 | func deserializeMessage(line string) (*transport.BaseJsonRpcMessage, error) { 123 | var request transport.BaseJSONRPCRequest 124 | if err := json.Unmarshal([]byte(line), &request); err == nil { 125 | //println("unmarshaled request:", spew.Sdump(request)) 126 | return transport.NewBaseMessageRequest(&request), nil 127 | } else { 128 | //println("unmarshaled request error:", err.Error()) 129 | } 130 | 131 | var notification transport.BaseJSONRPCNotification 132 | if err := json.Unmarshal([]byte(line), ¬ification); err == nil { 133 | return transport.NewBaseMessageNotification(¬ification), nil 134 | } else { 135 | //println("unmarshaled notification error:", err.Error()) 136 | } 137 | 138 | var response transport.BaseJSONRPCResponse 139 | if err := json.Unmarshal([]byte(line), &response); err == nil { 140 | return transport.NewBaseMessageResponse(&response), nil 141 | } else { 142 | //println("unmarshaled response error:", err.Error()) 143 | } 144 | 145 | var errorResponse transport.BaseJSONRPCError 146 | if err := json.Unmarshal([]byte(line), &errorResponse); err == nil { 147 | return transport.NewBaseMessageError(&errorResponse), nil 148 | } else { 149 | //println("unmarshaled error response error:", err.Error()) 150 | } 151 | 152 | // Must be a response 153 | return nil, errors.New("failed to unmarshal JSON-RPC message, unrecognized type") 154 | } 155 | -------------------------------------------------------------------------------- /transport/stdio/internal/stdio/stdio_test.go: -------------------------------------------------------------------------------- 1 | package stdio 2 | 3 | import ( 4 | "github.com/metoro-io/mcp-golang/transport" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | // TestReadBuffer tests the buffering functionality for JSON-RPC messages. 11 | // The ReadBuffer is crucial for handling streaming input and properly framing messages. 12 | // It verifies: 13 | // 1. Empty buffer handling returns nil message 14 | // 2. Incomplete messages are properly buffered 15 | // 3. Complete messages are correctly extracted 16 | // 4. Multiple message fragments are handled correctly 17 | // 5. Buffer clearing works as expected 18 | // This is a critical test as message framing is fundamental to the protocol. 19 | func TestReadBuffer(t *testing.T) { 20 | rb := NewReadBuffer() 21 | 22 | // Test empty buffer 23 | msg, err := rb.ReadMessage() 24 | if err != nil { 25 | t.Errorf("ReadMessage failed: %v", err) 26 | } 27 | if msg != nil { 28 | t.Errorf("Expected nil message, got %v", msg) 29 | } 30 | 31 | // Test incomplete message 32 | rb.Append([]byte(`{"jsonrpc": "2.0", "method": "test"`)) 33 | msg, err = rb.ReadMessage() 34 | if err != nil { 35 | t.Errorf("ReadMessage failed: %v", err) 36 | } 37 | if msg != nil { 38 | t.Errorf("Expected nil message, got %v", msg) 39 | } 40 | 41 | // Test complete message 42 | rb.Append([]byte(`, "params": {}}`)) 43 | rb.Append([]byte("\n")) 44 | msg, err = rb.ReadMessage() 45 | if err != nil { 46 | t.Errorf("ReadMessage failed: %v", err) 47 | } 48 | if msg == nil { 49 | t.Error("Expected message, got nil") 50 | } 51 | 52 | // Test clear 53 | rb.Clear() 54 | msg, err = rb.ReadMessage() 55 | if err != nil { 56 | t.Errorf("ReadMessage failed: %v", err) 57 | } 58 | if msg != nil { 59 | t.Errorf("Expected nil message, got %v", msg) 60 | } 61 | } 62 | 63 | // TestMessageDeserialization tests the parsing of different JSON-RPC message types. 64 | // Proper message type detection and parsing is critical for protocol operation. 65 | // It tests: 66 | // 1. Request messages (with method and ID) 67 | // 2. Notification messages (with method, no ID) 68 | // 3. Error responses (with error object) 69 | // 4. Success responses (with result) 70 | // Each message type must be correctly identified and parsed to maintain protocol integrity. 71 | func TestMessageDeserialization(t *testing.T) { 72 | tests := []struct { 73 | name string 74 | input string 75 | wantType transport.BaseMessageType 76 | }{ 77 | { 78 | name: "request", 79 | input: `{"jsonrpc": "2.0", "method": "test", "params": {}, "id": 1}`, 80 | wantType: transport.BaseMessageTypeJSONRPCRequestType, 81 | }, 82 | { 83 | name: "notification", 84 | input: `{"jsonrpc": "2.0", "method": "test", "params": {}}`, 85 | wantType: transport.BaseMessageTypeJSONRPCNotificationType, 86 | }, 87 | { 88 | name: "error", 89 | input: `{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": 1}`, 90 | wantType: transport.BaseMessageTypeJSONRPCErrorType, 91 | }, 92 | { 93 | name: "response", 94 | input: `{"jsonrpc": "2.0", "result": {}, "id": 1}`, 95 | wantType: transport.BaseMessageTypeJSONRPCResponseType, 96 | }, 97 | } 98 | 99 | for _, tt := range tests { 100 | t.Run(tt.name, func(t *testing.T) { 101 | msg, err := deserializeMessage(tt.input) 102 | if err != nil { 103 | t.Errorf("deserializeMessage failed: %v", err) 104 | } 105 | if msg == nil { 106 | t.Error("Expected message, got nil") 107 | } 108 | if msg.Type != tt.wantType { 109 | t.Errorf("Expected message type %s, got %s", tt.wantType, msg.Type) 110 | } 111 | }) 112 | } 113 | 114 | t.Run("request", func(t *testing.T) { 115 | msg, err := deserializeMessage(`{"jsonrpc":"2.0","id":1,"method":"test","params":{}}`) 116 | assert.NoError(t, err) 117 | assert.Equal(t, transport.BaseMessageTypeJSONRPCRequestType, msg.Type) 118 | assert.Equal(t, "2.0", msg.JsonRpcRequest.Jsonrpc) 119 | assert.Equal(t, "test", msg.JsonRpcRequest.Method) 120 | assert.Equal(t, transport.RequestId(1), msg.JsonRpcRequest.Id) 121 | }) 122 | 123 | t.Run("notification", func(t *testing.T) { 124 | msg, err := deserializeMessage(`{"jsonrpc":"2.0","method":"test","params":{}}`) 125 | assert.NoError(t, err) 126 | assert.Equal(t, transport.BaseMessageTypeJSONRPCNotificationType, msg.Type) 127 | assert.Equal(t, "2.0", msg.JsonRpcNotification.Jsonrpc) 128 | assert.Equal(t, "test", msg.JsonRpcNotification.Method) 129 | }) 130 | 131 | t.Run("error", func(t *testing.T) { 132 | msg, err := deserializeMessage(`{"jsonrpc":"2.0","id":1,"error":{"code":-32700,"message":"Parse error"}}`) 133 | assert.NoError(t, err) 134 | assert.Equal(t, transport.BaseMessageTypeJSONRPCErrorType, msg.Type) 135 | assert.Equal(t, "2.0", msg.JsonRpcError.Jsonrpc) 136 | assert.Equal(t, -32700, msg.JsonRpcError.Error.Code) 137 | assert.Equal(t, "Parse error", msg.JsonRpcError.Error.Message) 138 | }) 139 | } 140 | -------------------------------------------------------------------------------- /transport/stdio/stdio_server.go: -------------------------------------------------------------------------------- 1 | package stdio 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "os" 10 | "sync" 11 | 12 | "github.com/metoro-io/mcp-golang/transport" 13 | "github.com/metoro-io/mcp-golang/transport/stdio/internal/stdio" 14 | ) 15 | 16 | // StdioServerTransport implements server-side transport for stdio communication 17 | type StdioServerTransport struct { 18 | mu sync.Mutex 19 | started bool 20 | reader *bufio.Reader 21 | writer io.Writer 22 | readBuf *stdio.ReadBuffer 23 | onClose func() 24 | onError func(error) 25 | onMessage func(ctx context.Context, message *transport.BaseJsonRpcMessage) 26 | } 27 | 28 | // NewStdioServerTransport creates a new StdioServerTransport using os.Stdin and os.Stdout 29 | func NewStdioServerTransport() *StdioServerTransport { 30 | return NewStdioServerTransportWithIO(os.Stdin, os.Stdout) 31 | } 32 | 33 | // NewStdioServerTransportWithIO creates a new StdioServerTransport with custom io.Reader and io.Writer 34 | func NewStdioServerTransportWithIO(in io.Reader, out io.Writer) *StdioServerTransport { 35 | return &StdioServerTransport{ 36 | reader: bufio.NewReader(in), 37 | writer: out, 38 | readBuf: stdio.NewReadBuffer(), 39 | } 40 | } 41 | 42 | // Start begins listening for messages on stdin 43 | func (t *StdioServerTransport) Start(ctx context.Context) error { 44 | t.mu.Lock() 45 | if t.started { 46 | t.mu.Unlock() 47 | return fmt.Errorf("StdioServerTransport already started") 48 | } 49 | t.started = true 50 | t.mu.Unlock() 51 | 52 | go t.readLoop(ctx) 53 | return nil 54 | } 55 | 56 | // Close stops the transport and cleans up resources 57 | func (t *StdioServerTransport) Close() error { 58 | t.mu.Lock() 59 | defer t.mu.Unlock() 60 | 61 | t.started = false 62 | t.readBuf.Clear() 63 | if t.onClose != nil { 64 | t.onClose() 65 | } 66 | return nil 67 | } 68 | 69 | // Send sends a JSON-RPC message 70 | func (t *StdioServerTransport) Send(ctx context.Context, message *transport.BaseJsonRpcMessage) error { 71 | data, err := json.Marshal(message) 72 | if err != nil { 73 | return fmt.Errorf("failed to marshal message: %w", err) 74 | } 75 | data = append(data, '\n') 76 | 77 | //println("serialized message:", string(data)) 78 | 79 | t.mu.Lock() 80 | defer t.mu.Unlock() 81 | 82 | _, err = t.writer.Write(data) 83 | return err 84 | } 85 | 86 | // SetCloseHandler sets the handler for close events 87 | func (t *StdioServerTransport) SetCloseHandler(handler func()) { 88 | t.mu.Lock() 89 | defer t.mu.Unlock() 90 | t.onClose = handler 91 | } 92 | 93 | // SetErrorHandler sets the handler for error events 94 | func (t *StdioServerTransport) SetErrorHandler(handler func(error)) { 95 | t.mu.Lock() 96 | defer t.mu.Unlock() 97 | t.onError = handler 98 | } 99 | 100 | // SetMessageHandler sets the handler for incoming messages 101 | func (t *StdioServerTransport) SetMessageHandler(handler func(ctx context.Context, message *transport.BaseJsonRpcMessage)) { 102 | t.mu.Lock() 103 | defer t.mu.Unlock() 104 | t.onMessage = handler 105 | } 106 | 107 | func (t *StdioServerTransport) readLoop(ctx context.Context) { 108 | buffer := make([]byte, 4096) 109 | for { 110 | select { 111 | case <-ctx.Done(): 112 | t.Close() 113 | return 114 | default: 115 | t.mu.Lock() 116 | if !t.started { 117 | t.mu.Unlock() 118 | return 119 | } 120 | t.mu.Unlock() 121 | 122 | n, err := t.reader.Read(buffer) 123 | if err != nil { 124 | if err != io.EOF { 125 | t.handleError(fmt.Errorf("read error: %w", err)) 126 | } 127 | return 128 | } 129 | 130 | t.readBuf.Append(buffer[:n]) 131 | t.processReadBuffer() 132 | } 133 | } 134 | } 135 | 136 | func (t *StdioServerTransport) processReadBuffer() { 137 | for { 138 | msg, err := t.readBuf.ReadMessage() 139 | if err != nil { 140 | //println("error reading message:", err.Error()) 141 | t.handleError(err) 142 | return 143 | } 144 | if msg == nil { 145 | //println("no message") 146 | return 147 | } 148 | //println("received message:", spew.Sprint(msg)) 149 | t.handleMessage(msg) 150 | } 151 | } 152 | 153 | func (t *StdioServerTransport) handleError(err error) { 154 | t.mu.Lock() 155 | handler := t.onError 156 | t.mu.Unlock() 157 | 158 | if handler != nil { 159 | handler(err) 160 | } 161 | } 162 | 163 | func (t *StdioServerTransport) handleMessage(msg *transport.BaseJsonRpcMessage) { 164 | t.mu.Lock() 165 | handler := t.onMessage 166 | t.mu.Unlock() 167 | 168 | ctx := context.Background() 169 | 170 | if handler != nil { 171 | handler(ctx, msg) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /transport/stdio/stdio_server_test.go: -------------------------------------------------------------------------------- 1 | package stdio 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/metoro-io/mcp-golang/transport" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestStdioServerTransport(t *testing.T) { 16 | t.Run("basic message handling", func(t *testing.T) { 17 | in := &bytes.Buffer{} 18 | out := &bytes.Buffer{} 19 | tr := NewStdioServerTransportWithIO(in, out) 20 | 21 | var receivedMsg transport.JSONRPCMessage 22 | var wg sync.WaitGroup 23 | wg.Add(1) 24 | 25 | ctx := context.Background() 26 | 27 | tr.SetMessageHandler(func(ctx context.Context, msg *transport.BaseJsonRpcMessage) { 28 | receivedMsg = msg 29 | wg.Done() 30 | }) 31 | 32 | err := tr.Start(ctx) 33 | assert.NoError(t, err) 34 | 35 | // Write a test message to the input buffer 36 | testMsg := `{"jsonrpc": "2.0", "method": "test", "params": {}, "id": 1}` + "\n" 37 | _, err = in.Write([]byte(testMsg)) 38 | assert.NoError(t, err) 39 | 40 | // Wait for message processing with timeout 41 | done := make(chan struct{}) 42 | go func() { 43 | wg.Wait() 44 | close(done) 45 | }() 46 | 47 | select { 48 | case <-done: 49 | // Success 50 | case <-time.After(time.Second): 51 | t.Fatal("timeout waiting for message") 52 | } 53 | 54 | // Verify received message 55 | req, ok := receivedMsg.(*transport.BaseJsonRpcMessage) 56 | assert.True(t, ok) 57 | assert.True(t, req.Type == transport.BaseMessageTypeJSONRPCRequestType) 58 | assert.Equal(t, "test", req.JsonRpcRequest.Method) 59 | assert.Equal(t, transport.RequestId(1), req.JsonRpcRequest.Id) 60 | 61 | err = tr.Close() 62 | assert.NoError(t, err) 63 | }) 64 | 65 | t.Run("double start error", func(t *testing.T) { 66 | transport := NewStdioServerTransport() 67 | ctx := context.Background() 68 | err := transport.Start(ctx) 69 | assert.NoError(t, err) 70 | 71 | err = transport.Start(ctx) 72 | assert.Error(t, err) 73 | assert.Contains(t, err.Error(), "already started") 74 | 75 | err = transport.Close() 76 | assert.NoError(t, err) 77 | }) 78 | 79 | t.Run("send message", func(t *testing.T) { 80 | in := &bytes.Buffer{} 81 | out := &bytes.Buffer{} 82 | tr := NewStdioServerTransportWithIO(in, out) 83 | 84 | result := []byte(`{"status": "ok"}`) 85 | 86 | msg := &transport.BaseJSONRPCResponse{ 87 | Jsonrpc: "2.0", 88 | Result: result, 89 | Id: 1, 90 | } 91 | 92 | err := tr.Send(context.Background(), transport.NewBaseMessageResponse(msg)) 93 | assert.NoError(t, err) 94 | 95 | // Verify output contains the message and newline 96 | assert.Contains(t, out.String(), `{"id":1,"jsonrpc":"2.0","result":{"status":"ok"}}`) 97 | assert.Contains(t, out.String(), "\n") 98 | }) 99 | 100 | t.Run("error handling", func(t *testing.T) { 101 | in := &bytes.Buffer{} 102 | out := &bytes.Buffer{} 103 | transport := NewStdioServerTransportWithIO(in, out) 104 | 105 | var receivedErr error 106 | var wg sync.WaitGroup 107 | wg.Add(1) 108 | 109 | transport.SetErrorHandler(func(err error) { 110 | receivedErr = err 111 | wg.Done() 112 | }) 113 | 114 | ctx := context.Background() 115 | err := transport.Start(ctx) 116 | assert.NoError(t, err) 117 | 118 | // Write invalid JSON to trigger error 119 | _, err = in.Write([]byte(`{"invalid json`)) 120 | assert.NoError(t, err) 121 | 122 | // Write newline to complete the message 123 | _, err = in.Write([]byte("\n")) 124 | assert.NoError(t, err) 125 | 126 | // Wait for error handling with timeout 127 | done := make(chan struct{}) 128 | go func() { 129 | wg.Wait() 130 | close(done) 131 | }() 132 | 133 | select { 134 | case <-done: 135 | // Success 136 | case <-time.After(time.Second): 137 | t.Fatal("timeout waiting for error") 138 | } 139 | 140 | assert.NotNil(t, receivedErr) 141 | assert.Contains(t, receivedErr.Error(), "failed to unmarshal JSON-RPC message, unrecognized type") 142 | 143 | err = transport.Close() 144 | assert.NoError(t, err) 145 | }) 146 | 147 | t.Run("context cancellation", func(t *testing.T) { 148 | in := &bytes.Buffer{} 149 | out := &bytes.Buffer{} 150 | transport := NewStdioServerTransportWithIO(in, out) 151 | 152 | ctx, cancel := context.WithCancel(context.Background()) 153 | err := transport.Start(ctx) 154 | assert.NoError(t, err) 155 | 156 | var closed bool 157 | transport.SetCloseHandler(func() { 158 | closed = true 159 | }) 160 | 161 | // Cancel context and wait for close 162 | cancel() 163 | time.Sleep(100 * time.Millisecond) 164 | 165 | assert.True(t, closed, "transport should be closed after context cancellation") 166 | }) 167 | } 168 | -------------------------------------------------------------------------------- /transport/transport.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // Transport describes the minimal contract for a MCP transport that a client or server can communicate over. 8 | type Transport interface { 9 | // Start starts processing messages on the transport, including any connection steps that might need to be taken. 10 | // 11 | // This method should only be called after callbacks are installed, or else messages may be lost. 12 | // 13 | // NOTE: This method should not be called explicitly when using Client, Server, or Protocol classes, 14 | // as they will implicitly call start(). 15 | Start(ctx context.Context) error 16 | 17 | // Send sends a JSON-RPC message (request, notification or response). 18 | Send(ctx context.Context, message *BaseJsonRpcMessage) error 19 | 20 | // Close closes the connection. 21 | Close() error 22 | 23 | // SetCloseHandler sets the callback for when the connection is closed for any reason. 24 | // This should be invoked when Close() is called as well. 25 | SetCloseHandler(handler func()) 26 | 27 | // SetErrorHandler sets the callback for when an error occurs. 28 | // Note that errors are not necessarily fatal; they are used for reporting any kind of exceptional condition out of band. 29 | SetErrorHandler(handler func(error)) 30 | 31 | // SetMessageHandler sets the callback for when a message (request, notification or response) is received over the connection. 32 | // Partially deserializes the messages to pass a BaseJsonRpcMessage 33 | SetMessageHandler(handler func(ctx context.Context, message *BaseJsonRpcMessage)) 34 | } 35 | -------------------------------------------------------------------------------- /transport/types.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | ) 7 | 8 | type JSONRPCMessage interface{} 9 | 10 | type RequestId int64 11 | 12 | type BaseJSONRPCErrorInner struct { 13 | // The error type that occurred. 14 | Code int `json:"code" yaml:"code" mapstructure:"code"` 15 | 16 | // Additional information about the error. The value of this member is defined by 17 | // the sender (e.g. detailed error information, nested errors etc.). 18 | Data interface{} `json:"data,omitempty" yaml:"data,omitempty" mapstructure:"data,omitempty"` 19 | 20 | // A short description of the error. The message SHOULD be limited to a concise 21 | // single sentence. 22 | Message string `json:"message" yaml:"message" mapstructure:"message"` 23 | } 24 | 25 | // A response to a request that indicates an error occurred. 26 | type BaseJSONRPCError struct { 27 | // Error corresponds to the JSON schema field "error". 28 | Error BaseJSONRPCErrorInner `json:"error" yaml:"error" mapstructure:"error"` 29 | 30 | // Id corresponds to the JSON schema field "id". 31 | Id RequestId `json:"id" yaml:"id" mapstructure:"id"` 32 | 33 | // Jsonrpc corresponds to the JSON schema field "jsonrpc". 34 | Jsonrpc string `json:"jsonrpc" yaml:"jsonrpc" mapstructure:"jsonrpc"` 35 | } 36 | 37 | type BaseJSONRPCRequest struct { 38 | // Id corresponds to the JSON schema field "id". 39 | Id RequestId `json:"id" yaml:"id" mapstructure:"id"` 40 | 41 | // Jsonrpc corresponds to the JSON schema field "jsonrpc". 42 | Jsonrpc string `json:"jsonrpc" yaml:"jsonrpc" mapstructure:"jsonrpc"` 43 | 44 | // Method corresponds to the JSON schema field "method". 45 | Method string `json:"method" yaml:"method" mapstructure:"method"` 46 | 47 | // Params corresponds to the JSON schema field "params". 48 | // It is stored as a []byte to enable efficient marshaling and unmarshaling into custom types later on in the protocol 49 | Params json.RawMessage `json:"params,omitempty" yaml:"params,omitempty" mapstructure:"params,omitempty"` 50 | } 51 | 52 | // Custom Request unmarshaling 53 | // Requires an Id, Jsonrpc and Method 54 | func (m *BaseJSONRPCRequest) UnmarshalJSON(data []byte) error { 55 | required := struct { 56 | Id *RequestId `json:"id" yaml:"id" mapstructure:"id"` 57 | Jsonrpc *string `json:"jsonrpc" yaml:"jsonrpc" mapstructure:"jsonrpc"` 58 | Method *string `json:"method" yaml:"method" mapstructure:"method"` 59 | Params *json.RawMessage `json:"params" yaml:"params" mapstructure:"params"` 60 | }{} 61 | err := json.Unmarshal(data, &required) 62 | if err != nil { 63 | return err 64 | } 65 | if required.Id == nil { 66 | return errors.New("field id in BaseJSONRPCRequest: required") 67 | } 68 | if required.Jsonrpc == nil { 69 | return errors.New("field jsonrpc in BaseJSONRPCRequest: required") 70 | } 71 | if required.Method == nil { 72 | return errors.New("field method in BaseJSONRPCRequest: required") 73 | } 74 | if required.Params == nil { 75 | required.Params = new(json.RawMessage) 76 | } 77 | 78 | m.Id = *required.Id 79 | m.Jsonrpc = *required.Jsonrpc 80 | m.Method = *required.Method 81 | m.Params = *required.Params 82 | return nil 83 | } 84 | 85 | type BaseJSONRPCNotification struct { 86 | // Jsonrpc corresponds to the JSON schema field "jsonrpc". 87 | Jsonrpc string `json:"jsonrpc" yaml:"jsonrpc" mapstructure:"jsonrpc"` 88 | 89 | // Method corresponds to the JSON schema field "method". 90 | Method string `json:"method" yaml:"method" mapstructure:"method"` 91 | 92 | // Params corresponds to the JSON schema field "params". 93 | // It is stored as a []byte to enable efficient marshaling and unmarshaling into custom types later on in the protocol 94 | Params json.RawMessage `json:"params,omitempty" yaml:"params,omitempty" mapstructure:"params,omitempty"` 95 | } 96 | 97 | // Custom Notification unmarshaling 98 | // Requires a Jsonrpc and Method 99 | func (m *BaseJSONRPCNotification) UnmarshalJSON(data []byte) error { 100 | required := struct { 101 | Jsonrpc *string `json:"jsonrpc" yaml:"jsonrpc" mapstructure:"jsonrpc"` 102 | Method *string `json:"method" yaml:"method" mapstructure:"method"` 103 | Id *int64 `json:"id" yaml:"id" mapstructure:"id"` 104 | }{} 105 | err := json.Unmarshal(data, &required) 106 | if err != nil { 107 | return err 108 | } 109 | if required.Jsonrpc == nil { 110 | return errors.New("field jsonrpc in BaseJSONRPCNotification: required") 111 | } 112 | if required.Method == nil { 113 | return errors.New("field method in BaseJSONRPCNotification: required") 114 | } 115 | if required.Id != nil { 116 | return errors.New("field id in BaseJSONRPCNotification: not allowed") 117 | } 118 | m.Jsonrpc = *required.Jsonrpc 119 | m.Method = *required.Method 120 | return nil 121 | } 122 | 123 | type JsonRpcBody interface{} 124 | 125 | type BaseJSONRPCResponse struct { 126 | // Id corresponds to the JSON schema field "id". 127 | Id RequestId `json:"id" yaml:"id" mapstructure:"id"` 128 | 129 | // Jsonrpc corresponds to the JSON schema field "jsonrpc". 130 | Jsonrpc string `json:"jsonrpc" yaml:"jsonrpc" mapstructure:"jsonrpc"` 131 | 132 | // Result corresponds to the JSON schema field "result". 133 | Result json.RawMessage `json:"result" yaml:"result" mapstructure:"result"` 134 | } 135 | 136 | // Custom Response unmarshaling 137 | // Requires an Id, Jsonrpc and Result 138 | func (m *BaseJSONRPCResponse) UnmarshalJSON(data []byte) error { 139 | required := struct { 140 | Id *RequestId `json:"id" yaml:"id" mapstructure:"id"` 141 | Jsonrpc *string `json:"jsonrpc" yaml:"jsonrpc" mapstructure:"jsonrpc"` 142 | Result *json.RawMessage `json:"result" yaml:"result" mapstructure:"result"` 143 | }{} 144 | err := json.Unmarshal(data, &required) 145 | if err != nil { 146 | return err 147 | } 148 | if required.Id == nil { 149 | return errors.New("field id in BaseJSONRPCResponse: required") 150 | } 151 | if required.Jsonrpc == nil { 152 | return errors.New("field jsonrpc in BaseJSONRPCResponse: required") 153 | } 154 | if required.Result == nil { 155 | return errors.New("field result in BaseJSONRPCResponse: required") 156 | } 157 | m.Id = *required.Id 158 | m.Jsonrpc = *required.Jsonrpc 159 | m.Result = *required.Result 160 | 161 | return err 162 | } 163 | 164 | type BaseMessageType string 165 | 166 | const ( 167 | BaseMessageTypeJSONRPCRequestType BaseMessageType = "request" 168 | BaseMessageTypeJSONRPCNotificationType BaseMessageType = "notification" 169 | BaseMessageTypeJSONRPCResponseType BaseMessageType = "response" 170 | BaseMessageTypeJSONRPCErrorType BaseMessageType = "error" 171 | ) 172 | 173 | type BaseJsonRpcMessage struct { 174 | Type BaseMessageType 175 | JsonRpcRequest *BaseJSONRPCRequest 176 | JsonRpcNotification *BaseJSONRPCNotification 177 | JsonRpcResponse *BaseJSONRPCResponse 178 | JsonRpcError *BaseJSONRPCError 179 | } 180 | 181 | func (m *BaseJsonRpcMessage) MarshalJSON() ([]byte, error) { 182 | switch m.Type { 183 | case BaseMessageTypeJSONRPCRequestType: 184 | return json.Marshal(m.JsonRpcRequest) 185 | case BaseMessageTypeJSONRPCNotificationType: 186 | return json.Marshal(m.JsonRpcNotification) 187 | case BaseMessageTypeJSONRPCResponseType: 188 | return json.Marshal(m.JsonRpcResponse) 189 | case BaseMessageTypeJSONRPCErrorType: 190 | return json.Marshal(m.JsonRpcError) 191 | default: 192 | return nil, errors.New("unknown message type, couldn't marshal") 193 | } 194 | } 195 | 196 | func NewBaseMessageNotification(notification *BaseJSONRPCNotification) *BaseJsonRpcMessage { 197 | return &BaseJsonRpcMessage{ 198 | Type: BaseMessageTypeJSONRPCNotificationType, 199 | JsonRpcNotification: notification, 200 | } 201 | } 202 | 203 | func NewBaseMessageRequest(request *BaseJSONRPCRequest) *BaseJsonRpcMessage { 204 | return &BaseJsonRpcMessage{ 205 | Type: BaseMessageTypeJSONRPCRequestType, 206 | JsonRpcRequest: request, 207 | } 208 | } 209 | 210 | func NewBaseMessageResponse(response *BaseJSONRPCResponse) *BaseJsonRpcMessage { 211 | return &BaseJsonRpcMessage{ 212 | Type: BaseMessageTypeJSONRPCResponseType, 213 | JsonRpcResponse: response, 214 | } 215 | } 216 | 217 | func NewBaseMessageError(error *BaseJSONRPCError) *BaseJsonRpcMessage { 218 | return &BaseJsonRpcMessage{ 219 | Type: BaseMessageTypeJSONRPCErrorType, 220 | JsonRpcError: &BaseJSONRPCError{ 221 | Error: error.Error, 222 | Id: error.Id, 223 | Jsonrpc: error.Jsonrpc, 224 | }, 225 | } 226 | } 227 | --------------------------------------------------------------------------------