Skip to content

Commit

Permalink
✨ Add registries
Browse files Browse the repository at this point in the history
  • Loading branch information
wesen committed Dec 15, 2024
1 parent 6c35983 commit 06180b0
Show file tree
Hide file tree
Showing 11 changed files with 907 additions and 134 deletions.
166 changes: 123 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,68 +6,131 @@ A Go implementation of the Model Context Protocol (MCP), providing a framework f

This project implements the [Model Context Protocol](https://github.com/modelcontextprotocol/specification), which enables standardized communication between AI applications and language models. The implementation includes:

- Core protocol message types
- Core protocol message types and interfaces
- A modular registry system for managing prompts, resources, and tools
- Thread-safe provider implementations
- A stdio server implementation
- Support for prompts and logging capabilities
- Support for custom handlers and subscriptions

## Architecture

The project follows a modular, provider-based architecture with these main components:

1. Protocol Types (`pkg/protocol/types.go`)
2. Registry System
- Prompts Registry (`pkg/prompts/registry.go`)
- Resources Registry (`pkg/resources/registry.go`)
- Tools Registry (`pkg/tools/registry.go`)
3. Server Implementation (`pkg/server/server.go`)
4. Error Handling (`pkg/registry.go`)

For detailed architecture documentation, see [Architecture Documentation](pkg/doc/architecture.md).

## Features

### Registry System

The registry system provides thread-safe management of prompts, resources, and tools:

```go
// Create registries
promptRegistry := prompts.NewRegistry()
resourceRegistry := resources.NewRegistry()
toolRegistry := tools.NewRegistry()

// Register a prompt with custom handler
promptRegistry.RegisterPromptWithHandler(protocol.Prompt{
Name: "hello",
Description: "A simple greeting prompt",
Arguments: []protocol.PromptArgument{
{
Name: "name",
Description: "Name to greet",
Required: false,
},
},
}, func(prompt protocol.Prompt, args map[string]string) (*protocol.PromptMessage, error) {
return &protocol.PromptMessage{
Role: "user",
Content: protocol.PromptContent{
Type: "text",
Text: fmt.Sprintf("Hello, %s!", args["name"]),
},
}, nil
})
```

### Custom Handlers

Each registry supports custom handlers for flexible behavior:

- **Prompts**: Custom message generation
- **Resources**: Custom content providers with subscription support
- **Tools**: Custom tool execution handlers

### Error Handling

Standardized error handling with JSON-RPC compatible error codes:

```go
var (
ErrPromptNotFound = NewError("prompt not found", -32000)
ErrResourceNotFound = NewError("resource not found", -32001)
ErrToolNotFound = NewError("tool not found", -32002)
ErrNotImplemented = NewError("not implemented", -32003)
)
```

## Example Server

The project includes an example stdio server that demonstrates basic MCP functionality:
The project includes an example stdio server that demonstrates the registry system:

```go
package main

import (
"github.com/go-go-golems/go-mcp/pkg"
"io"

"github.com/go-go-golems/go-mcp/pkg/prompts"
"github.com/go-go-golems/go-mcp/pkg/protocol"
"github.com/go-go-golems/go-mcp/pkg/resources"
"github.com/go-go-golems/go-mcp/pkg/server"
"github.com/go-go-golems/go-mcp/pkg/tools"
"github.com/rs/zerolog/log"
)

func main() {
server := NewServer()
if err := server.Start(); err != nil && err != io.EOF {
srv := server.NewServer()

// Create registries
promptRegistry := prompts.NewRegistry()
resourceRegistry := resources.NewRegistry()
toolRegistry := tools.NewRegistry()

// Register with server
srv.GetRegistry().RegisterPromptProvider(promptRegistry)
srv.GetRegistry().RegisterResourceProvider(resourceRegistry)
srv.GetRegistry().RegisterToolProvider(toolRegistry)

if err := srv.Start(); err != nil && err != io.EOF {
log.Fatal().Err(err).Msg("Server error")
}
}
```

### Features

The example server implements:

- JSON-RPC 2.0 message handling
- Protocol version negotiation
- Capability declaration
- Structured logging
- Simple prompt system

### Supported Methods

The server implements the MCP specification methods:

- `initialize` - Protocol initialization and capability negotiation
- `ping` - Connection health check
- `prompts/list` - List available prompts
- `prompts/get` - Retrieve prompt content

### Example Prompt

The server includes a simple prompt that demonstrates prompt arguments:

```json
{
"name": "simple",
"description": "A simple prompt that can take optional context and topic arguments",
"arguments": [
{
"name": "context",
"description": "Additional context to consider",
"required": false
},
{
"name": "topic",
"description": "Specific topic to focus on",
"required": false
}
]
}
```
- `resources/list` - List available resources
- `resources/read` - Read resource content
- `resources/subscribe` - Subscribe to resource updates
- `tools/list` - List available tools
- `tools/call` - Execute a tool

## Usage

Expand All @@ -76,7 +139,7 @@ The server includes a simple prompt that demonstrates prompt arguments:
Build and run the example stdio server:

```bash
go build -o stdio-server go/cmd/stdio-server/main.go
go build -o stdio-server cmd/stdio-server/main.go
./stdio-server
```

Expand All @@ -91,7 +154,18 @@ The server accepts JSON-RPC messages on stdin and writes responses to stdout. Ex
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"capabilities": {
"prompts": {
"listChanged": true
},
"resources": {
"subscribe": true,
"listChanged": true
},
"tools": {
"listChanged": true
}
},
"clientInfo": {
"name": "example-client",
"version": "1.0.0"
Expand All @@ -104,7 +178,13 @@ The server accepts JSON-RPC messages on stdin and writes responses to stdout. Ex

### Project Structure

- `pkg/` - Core protocol types and utilities
- `pkg/`
- `protocol/` - Core protocol types and interfaces
- `prompts/` - Prompt registry and handlers
- `resources/` - Resource registry and handlers
- `tools/` - Tool registry and handlers
- `server/` - Server implementation
- `doc/` - Documentation
- `cmd/stdio-server/` - Example stdio server implementation

### Dependencies
Expand All @@ -114,7 +194,7 @@ The server accepts JSON-RPC messages on stdin and writes responses to stdout. Ex

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

## License

Expand Down
34 changes: 31 additions & 3 deletions cmd/stdio-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,44 @@ package main
import (
"io"

"github.com/go-go-golems/go-mcp/pkg/prompts"
"github.com/go-go-golems/go-mcp/pkg/protocol"
"github.com/go-go-golems/go-mcp/pkg/resources"
"github.com/go-go-golems/go-mcp/pkg/server"
"github.com/go-go-golems/go-mcp/pkg/tools"
"github.com/rs/zerolog/log"
)

func main() {
srv := server.NewServer()

// Register the simple provider
simpleProvider := NewSimpleProvider()
srv.GetRegistry().RegisterPromptProvider(simpleProvider)
// Create registries
promptRegistry := prompts.NewRegistry()
resourceRegistry := resources.NewRegistry()
toolRegistry := tools.NewRegistry()

// Register a simple prompt directly
promptRegistry.RegisterPrompt(protocol.Prompt{
Name: "simple",
Description: "A simple prompt that can take optional context and topic arguments",
Arguments: []protocol.PromptArgument{
{
Name: "context",
Description: "Additional context to consider",
Required: false,
},
{
Name: "topic",
Description: "Specific topic to focus on",
Required: false,
},
},
})

// Register registries with the server
srv.GetRegistry().RegisterPromptProvider(promptRegistry)
srv.GetRegistry().RegisterResourceProvider(resourceRegistry)
srv.GetRegistry().RegisterToolProvider(toolRegistry)

if err := srv.Start(); err != nil && err != io.EOF {
log.Fatal().Err(err).Msg("Server error")
Expand Down
91 changes: 32 additions & 59 deletions cmd/stdio-server/provider.go
Original file line number Diff line number Diff line change
@@ -1,75 +1,48 @@
package main

import (
"fmt"

"github.com/go-go-golems/go-mcp/pkg"
"github.com/go-go-golems/go-mcp/pkg/prompts"
"github.com/go-go-golems/go-mcp/pkg/protocol"
)

// SimpleProvider implements a basic provider with a single prompt
type SimpleProvider struct{}
type SimpleProvider struct {
registry *prompts.Registry
}

// NewSimpleProvider creates a new simple provider
func NewSimpleProvider() *SimpleProvider {
return &SimpleProvider{}
}

// ListPrompts returns a list of available prompts
func (p *SimpleProvider) ListPrompts(cursor string) ([]pkg.Prompt, string, error) {
return []pkg.Prompt{
{
Name: "simple",
Description: "A simple prompt that can take optional context and topic " +
"arguments",
Arguments: []pkg.PromptArgument{
{
Name: "context",
Description: "Additional context to consider",
Required: false,
},
{
Name: "topic",
Description: "Specific topic to focus on",
Required: false,
},
registry := prompts.NewRegistry()

// Register our simple prompt
registry.RegisterPrompt(protocol.Prompt{
Name: "simple",
Description: "A simple prompt that can take optional context and topic arguments",
Arguments: []protocol.PromptArgument{
{
Name: "context",
Description: "Additional context to consider",
Required: false,
},
{
Name: "topic",
Description: "Specific topic to focus on",
Required: false,
},
},
}, "", nil
}

// GetPrompt retrieves a specific prompt with the given arguments
func (p *SimpleProvider) GetPrompt(name string, arguments map[string]string) (*pkg.PromptMessage, error) {
if name != "simple" {
return nil, fmt.Errorf("unknown prompt: %s", name)
}

// Create messages based on arguments
messages := []pkg.PromptMessage{}
})

// Add context if provided
if context, ok := arguments["context"]; ok {
messages = append(messages, pkg.PromptMessage{
Role: "user",
Content: pkg.PromptContent{
Type: "text",
Text: fmt.Sprintf("Here is some relevant context: %s", context),
},
})
return &SimpleProvider{
registry: registry,
}
}

// Add main prompt
prompt := "Please help me with "
if topic, ok := arguments["topic"]; ok {
prompt += fmt.Sprintf("the following topic: %s", topic)
} else {
prompt += "whatever questions I may have."
}
// ListPrompts returns a list of available prompts
func (p *SimpleProvider) ListPrompts(cursor string) ([]protocol.Prompt, string, error) {
return p.registry.ListPrompts(cursor)
}

return &pkg.PromptMessage{
Role: "user",
Content: pkg.PromptContent{
Type: "text",
Text: prompt,
},
}, nil
// GetPrompt retrieves a specific prompt with the given arguments
func (p *SimpleProvider) GetPrompt(name string, arguments map[string]string) (*protocol.PromptMessage, error) {
return p.registry.GetPrompt(name, arguments)
}
Loading

0 comments on commit 06180b0

Please sign in to comment.