├── .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 |
3 |
4 |
5 |
6 |
7 | 
8 | 
9 | 
10 | 
11 | 
12 | 
13 | 
14 | [](https://pkg.go.dev/github.com/metoro-io/mcp-golang)
15 | [](https://goreportcard.com/report/github.com/metoro-io/mcp-golang)
16 | 
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 |
--------------------------------------------------------------------------------
/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 | 
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 | VIDEO
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 | VIDEO
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 |
11 |
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 |
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 |
--------------------------------------------------------------------------------