Skip to content

Commit 7b2f2f0

Browse files
authored
add prompt capability (#2)
1 parent daebabe commit 7b2f2f0

20 files changed

+779
-60
lines changed

README.md

Lines changed: 234 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,240 @@
11
# gomcp
2-
Unofficial Golang SDK for Anthropic Model Context Protocol
32

4-
This is still very much a work in progress.
3+
## Description
4+
An unofficial Golang implementation of the Model Context Protocol defined by Anthropic.
55

6-
You'll find a presentation of the project in this [blog post](http://pcarion.com/blog/go_model_context_protocol/)
6+
The officialannouncement of the Model Context Protocol is [here](https://www.anthropic.com/news/model-context-protocol).
77

8+
The Model Context Protocol (MCP) provides a standardized, secure mechanism for AI models to interact with external tools and data sources.
89

9-
# Reference documentation
10+
By defining a precise interface and communication framework, MCP allows AI assistants, such as the Claude desktop application, to safely
11+
extend their capabilities.
1012

11-
You'll find the documentation of the Model Context Protocol [here](https://modelcontextprotocol.io/introduction)
13+
## Reference documentation
14+
15+
* the full documentation of the Model Context Protocol is [here](https://modelcontextprotocol.io/introduction)
16+
* the official TypeScript SDK is available[here](https://github.com/modelcontextprotocol/typescript-sdk)
17+
* the official Python SDK is available[here](https://github.com/modelcontextprotocol/python-sdk)
18+
19+
## Installation
20+
21+
```
22+
go get github.com/llmcontext/gomcp
23+
```
24+
25+
Direct dependencies:
26+
27+
* [github.com/invopop/jsonschema](https://github.com/invopop/jsonschema): to generate JSON Schemas from Go types through reflection
28+
* [github.com/xeipuuv/gojsonschema](https://github.com/xeipuuv/gojsonschema): for JSON schema validation
29+
* [gopkg.in/yaml.v3](https://github.com/go-yaml/yaml): for YAML parsing of the prompts definitionfile
30+
* [github.com/stretchr/testify](https://github.com/stretchr/testify): for testing
31+
* [go.uber.org/zap](https://github.com/uber-go/zap): for logging
32+
33+
## Usage
34+
35+
Let's consider a simple example where we want to use the `mcp` package to create a server that can retrieve the content of a Notion page so that you can use it in a Claude chat.
36+
37+
The way to do this is to define a set of tools that can be exposed to the LLM through the Model Context Protocol and to implement them in Go.
38+
39+
The first step is to define the tools that will be exposed to the LLM.
40+
41+
In Mcp, you define a set of `Tool providers` and each provider is a set of `Tools`.
42+
43+
In our case, we have a single provider called `notion` that has a single tool to retrieve the content of a Notion page.
44+
45+
### configuration file
46+
47+
Once compiled, the `mcp` command needs a configuration file to start the server.
48+
49+
An example of configuration file would be:
50+
51+
```json
52+
{
53+
"serverInfo": {
54+
"name": "gomcp",
55+
"version": "0.1.0"
56+
},
57+
"logging": {
58+
"file": "/var/log/gomcp/mcpnotion.log",
59+
"level": "debug",
60+
"withStderr": false
61+
},
62+
"prompts": {
63+
"file": "/etc/gomcp/prompts.yaml"
64+
},
65+
"tools": [
66+
{
67+
"name": "notion",
68+
"description": "Get a notion document",
69+
"configuration": {
70+
"notionToken": "ntn_<redacted>"
71+
}
72+
}
73+
]
74+
}
75+
```
76+
77+
The `serverInfo` section is used to identify the server and its version, it is mandatory as they are used in the MCP protocolto identify the server.
78+
79+
The `logging` section is used to configure the logging system. The `file` field is the path to the log file, the `level` field is the logging level (debug, info, warn, error) and the `withStderr` field is used to redirect the logging to the standard error stream.
80+
81+
The `prompts` section is used to define the path to the YAML file containing the prompts to expose to the LLM. See below for a description of the YAML syntax to define the prompts.
82+
83+
The `tools` section is used to define the tools that will be exposed to the LLM. This is an array of tool providers, each provider is an object with a `name` and a `description` field. The `configuration` field is an object that contains the configuration for the tool provider.
84+
85+
In our case, we have a single tool provider called `notion` that has a single tool to retrieve the content of a Notion page.
86+
87+
The configuration for the `notion` tool provider is the Notion token.
88+
89+
This configuration must be backed by a Golang struct that will be used to parse the configuration file:
90+
91+
```go
92+
type NotionGetDocumentConfiguration struct {
93+
NotionToken string `json:"notionToken" jsonschema_description:"the notion token for the Notion client."`
94+
}
95+
```
96+
The tags here (`json` and `jsonschema_description`) are used to generate the JSON Schema for the configuration data.
97+
If the configuration is invalid, the `mcp` command will fail to start.
98+
99+
You then create a function that will use those configuration data to generate a `Tool Context`:
100+
101+
```go
102+
func NotionToolInit(ctx context.Context, config *NotionGetDocumentConfiguration) (*NotionGetDocumentContext, error) {
103+
client := notionapi.NewClient(notionapi.Token(config.NotionToken))
104+
105+
// we need to initialize the Notion client
106+
return &NotionGetDocumentContext{NotionClient: client}, nil
107+
}
108+
```
109+
110+
And the definition of the `Tool Context`:
111+
112+
```go
113+
type NotionGetDocumentContext struct {
114+
// The Notion client.
115+
NotionClient *notionapi.Client
116+
}
117+
```
118+
This time, you don't need to add tags to your type as this struct is internal to the tool provider: the instance of this struct is created by the `ToolInit` function and is passed to the functions implementing the tool(s).
119+
120+
A tool function is defined like this:
121+
122+
```go
123+
func NotionGetPage(
124+
ctx context.Context,
125+
toolCtx *NotionGetDocumentContext,
126+
input *NotionGetDocumentInput,
127+
output types.ToolCallResult) error {
128+
logger := gomcp.GetLogger(ctx)
129+
logger.Info("NotionGetPage", types.LogArg{
130+
"pageId": input.PageId,
131+
})
132+
133+
content, err := getPageContent(ctx, toolCtx.NotionClient, input.PageId)
134+
if err != nil {
135+
return err
136+
}
137+
output.AddTextContent(strings.Join(content, "\n"))
138+
139+
return nil
140+
}
141+
```
142+
The first parameter is the context, it is mandatory as it is used to retrieve the logger.
143+
144+
The second parameter is the tool context, it is the instance of the struct created by the `ToolInit` function.
145+
146+
The third parameter is the input, it is an object that contains the input data for the tool call. Those input parameter are provided by the LLM when the tool is called.
147+
148+
The last parameter is the output, it is an interface that allows the function to construct the data it wants to return to the LLM.
149+
150+
Here again, the input type must be tagged appropriately:
151+
152+
```go
153+
type NotionGetDocumentInput struct {
154+
PageId string `json:"pageId" jsonschema_description:"the ID of the Notion page to retrieve."`
155+
}
156+
```
157+
158+
Those tags will be used to generate the JSON Schema for the input data:
159+
* they will be returned to the LLM during the discovery phase of the MCP protocol
160+
* they will be used to validate the input data when the tool is called
161+
162+
Once those typea and functions are defined, you can bind them in the MCP server by calling the `RegisterTool` function:
163+
164+
165+
```go
166+
func RegisterTools(toolRegistry types.ToolRegistry) error {
167+
toolProvider, err := toolRegistry.DeclareToolProvider("notion", NotionToolInit)
168+
if err != nil {
169+
return err
170+
}
171+
err = toolProvider.AddTool("notion_get_page", "Get the markdown content of a notion page", NotionGetPage)
172+
if err != nil {
173+
return err
174+
}
175+
return nil
176+
}
177+
```
178+
179+
This `RegisterTools` function is called from the `main()` function of your MCP server:
180+
181+
```go
182+
package main
183+
184+
import (
185+
"flag"
186+
"fmt"
187+
"os"
188+
189+
"github.com/llmcontext/gomcp"
190+
"github.com/llmcontext/mcpnotion/tools"
191+
)
192+
193+
func main() {
194+
configFile := flag.String("configFile", "", "config file path (required)")
195+
flag.Parse()
196+
197+
if *configFile == "" {
198+
fmt.Println("Config file is required")
199+
flag.PrintDefaults()
200+
os.Exit(1)
201+
}
202+
203+
mcp, err := gomcp.NewModelContextProtocolServer(*configFile)
204+
if err != nil {
205+
fmt.Println("Error creating MCP server:", err)
206+
os.Exit(1)
207+
}
208+
toolRegistry := mcp.GetToolRegistry()
209+
210+
err = tools.RegisterTools(toolRegistry)
211+
if err != nil {
212+
fmt.Println("Error registering tools:", err)
213+
os.Exit(1)
214+
}
215+
216+
transport := mcp.StdioTransport()
217+
218+
mcp.Start(transport)
219+
}
220+
```
221+
222+
* `gomcp.NewModelContextProtocolServer(*configFile)` creates a new MCP server with the configuration file
223+
* `mcp.GetToolRegistry()` returns the tool registry, it is used to register the tools. The `tools.RegisterTools` is the function we defined earlier and that binds the tools to the MCP server.
224+
* `mcp.StdioTransport()` creates a new transport based on standard input/output streams. That's the transport used to integrate with the Claude desktop application.
225+
* `mcp.Start(transport)` starts the MCP server with the given transport
226+
227+
228+
## integration with Claude desktop application
229+
230+
## Changelog
231+
232+
### [0.1.0](https://github.com/llmcontext/gomcp/tree/v0.1.0)
233+
234+
- Initial release
235+
- Support Tools for the Model Context Protocol
236+
237+
### [0.2.0](https://github.com/llmcontext/gomcp/tree/v0.2.0)
238+
239+
- Change signature of `mcp.Start(serverName, serverVersion, transport)` to `mcp.Start(transport)`, the server name and version are now read from the configuration file
240+
- Add support for prompts stored in a YAML file. File path is read from the configuration file.

config/config.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import (
1010
)
1111

1212
type Config struct {
13-
Logging LoggingInfo `json:"logging,omitempty"`
14-
ServerInfo ServerInfo `json:"serverInfo"`
15-
Tools []ToolConfig `json:"tools"`
13+
Logging LoggingInfo `json:"logging,omitempty"`
14+
ServerInfo ServerInfo `json:"serverInfo"`
15+
Tools []ToolConfig `json:"tools"`
16+
Prompts *PromptConfig `json:"prompts,omitempty"`
1617
}
1718

1819
type ServerInfo struct {
@@ -34,6 +35,10 @@ type LoggingInfo struct {
3435
WithStderr bool `json:"withStderr,omitempty"`
3536
}
3637

38+
type PromptConfig struct {
39+
File string `json:"file"`
40+
}
41+
3742
// LoadConfig loads the configuration from a file
3843
func LoadConfig(configFilePath string) (*Config, error) {
3944

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/stretchr/testify v1.8.1
88
github.com/xeipuuv/gojsonschema v1.2.0
99
go.uber.org/zap v1.27.0
10+
gopkg.in/yaml.v3 v3.0.1
1011
)
1112

1213
require (
@@ -19,5 +20,4 @@ require (
1920
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
2021
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
2122
go.uber.org/multierr v1.10.0 // indirect
22-
gopkg.in/yaml.v3 v3.0.1 // indirect
2323
)

mcp/mcp.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@ import (
55

66
"github.com/llmcontext/gomcp/config"
77
"github.com/llmcontext/gomcp/logger"
8+
"github.com/llmcontext/gomcp/prompts"
89
"github.com/llmcontext/gomcp/server"
910
"github.com/llmcontext/gomcp/tools"
1011
"github.com/llmcontext/gomcp/transport"
1112
"github.com/llmcontext/gomcp/types"
1213
)
1314

1415
type ModelContextProtocolImpl struct {
15-
toolsRegistry *tools.ToolsRegistry
16-
config *config.Config
16+
config *config.Config
17+
toolsRegistry *tools.ToolsRegistry
18+
promptsRegistry *prompts.PromptsRegistry
1719
}
1820

1921
func NewModelContextProtocolServer(configFilePath string) (*ModelContextProtocolImpl, error) {
@@ -32,14 +34,24 @@ func NewModelContextProtocolServer(configFilePath string) (*ModelContextProtocol
3234
// Initialize tools registry
3335
toolsRegistry := tools.NewToolsRegistry()
3436

37+
// Initialize prompts registry
38+
promptsRegistry := prompts.NewEmptyPromptsRegistry()
39+
if config.Prompts != nil {
40+
promptsRegistry, err = prompts.NewPromptsRegistry(config.Prompts.File)
41+
if err != nil {
42+
return nil, fmt.Errorf("failed to initialize prompts registry: %v", err)
43+
}
44+
}
45+
3546
return &ModelContextProtocolImpl{
36-
toolsRegistry: toolsRegistry,
37-
config: config,
47+
config: config,
48+
toolsRegistry: toolsRegistry,
49+
promptsRegistry: promptsRegistry,
3850
}, nil
3951
}
4052

4153
func (mcp *ModelContextProtocolImpl) StdioTransport() types.Transport {
42-
transport := transport.NewStdioTransport()
54+
transport := transport.NewStdioTransportWithDebug()
4355
return transport
4456
}
4557

@@ -53,7 +65,7 @@ func (mcp *ModelContextProtocolImpl) DeclareToolProvider(toolName string, toolIn
5365
return toolProvider, nil
5466
}
5567

56-
func (mcp *ModelContextProtocolImpl) Start(serverName string, serverVersion string, transport types.Transport) error {
68+
func (mcp *ModelContextProtocolImpl) Start(transport types.Transport) error {
5769
// All the tools are initialized, we can prepare the tools registry
5870
// so that it can be used by the server
5971
err := mcp.toolsRegistry.Prepare(mcp.config.Tools)
@@ -62,7 +74,9 @@ func (mcp *ModelContextProtocolImpl) Start(serverName string, serverVersion stri
6274
}
6375

6476
// Initialize server
65-
server := server.NewMCPServer(transport, mcp.toolsRegistry, serverName, serverVersion)
77+
server := server.NewMCPServer(transport, mcp.toolsRegistry, mcp.promptsRegistry,
78+
mcp.config.ServerInfo.Name,
79+
mcp.config.ServerInfo.Version)
6680

6781
// Start server
6882
err = server.Start()

0 commit comments

Comments
 (0)