Skip to content

Commit

Permalink
🎨 Improve structure / format of the code: MCP client support
Browse files Browse the repository at this point in the history
  • Loading branch information
k33g committed Feb 8, 2025
1 parent 20f3622 commit b3eea7a
Show file tree
Hide file tree
Showing 17 changed files with 570 additions and 1 deletion.
21 changes: 20 additions & 1 deletion LAST_RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,27 @@ if noHostErr, ok := err.(*completion.NoSuchOllamaHostError); ok {
}
```

### First MCP support

### MCP
Integration of `github.com/mark3labs/mcp-go/mcp` and `github.com/mark3labs/mcp-go/client` (this is a work in progress 🚧)

#### Helpers

- `mcphelpers.GetMCPClient(ctx context.Context, command string, env []string, args ...string) (*client.StdioMCPClient, *mcp.InitializeResult, error)`
- `mcphelpers.GetTools(mcpClient *client.StdioMCPClient) ([]llm.Tool, error)`
- `tools.ConvertMCPTools` to convert the MCP tools list to a list compliant with Ollama LLM tools. (used by `GetTools`)
- `mcphelpers.CallTool(ctx context.Context, mcpClient *client.StdioMCPClient, functionName string, arguments map[string]interface{}) (*mcp.CallToolResult, error)`
- `mcphelpers.GetTextFromResult(mcpResult *mcp.CallToolResult) (string, error)`

> See this example: `67-mcp` (an example of a MCP server is provided)
#### Error management (specific type errors)

- `MCPClientCreationError`
- `MCPClientInitializationError`
- `MCPGetToolsError`
- `MCPToolCallError`
- `MCPResultExtractionError`


## v0.2.3 🥧 [pie]
Expand Down
5 changes: 5 additions & 0 deletions examples/67-mcp/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# if working from a container
#OLLAMA_HOST=http://host.docker.internal:11434
OLLAMA_HOST=http://localhost:11434
LLM_WITH_TOOLS_SUPPORT=qwen2.5:0.5b
LLM_CHAT=qwen2.5:0.5b
6 changes: 6 additions & 0 deletions examples/67-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# MCP: model context protocol

First, build the MCP server, see: [/mcp-server/README.md](/mcp-server/README.md), then, run:
```bash
go run main.go
```
10 changes: 10 additions & 0 deletions examples/67-mcp/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module 67-mcp

go 1.23.1

require (
github.com/mark3labs/mcp-go v0.8.3
github.com/parakeet-nest/parakeet v0.0.0-00010101000000-000000000000
)

replace github.com/parakeet-nest/parakeet => ../..
4 changes: 4 additions & 0 deletions examples/67-mcp/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mark3labs/mcp-go v0.8.3 h1:IzlyN8BaP4YwUMUDqxOGJhGdZXEDQiAPX43dNPgnzrg=
github.com/mark3labs/mcp-go v0.8.3/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE=
128 changes: 128 additions & 0 deletions examples/67-mcp/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package main

import (
"context"
"fmt"
"log"
"os"
"time"

"github.com/joho/godotenv"
"github.com/parakeet-nest/parakeet/completion"
"github.com/parakeet-nest/parakeet/enums/option"
"github.com/parakeet-nest/parakeet/llm"
"github.com/parakeet-nest/parakeet/mcphelpers"
)

func main() {

err := godotenv.Load()
if err != nil {
log.Fatalln("😡", err)
}

ollamaUrl := os.Getenv("OLLAMA_HOST")
if ollamaUrl == "" {
ollamaUrl = "http://localhost:11434"
}

modelWithToolsSupport := os.Getenv("LLM_WITH_TOOLS_SUPPORT")
if modelWithToolsSupport == "" {
modelWithToolsSupport = "qwen2.5:0.5b"
}

chatModel := os.Getenv("LLM_CHAT")
if chatModel == "" {
chatModel = "qwen2.5:0.5b"
}

// Create context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// Create a new mcp client
mcpClient, _, err := mcphelpers.GetMCPClient(ctx, "docker",
[]string{}, // Empty ENV
"run",
"--rm",
"-i",
"mcp-curl",
)

if err != nil {
log.Fatalln("😡", err)
}
defer mcpClient.Close()

ollamaTools, err := mcphelpers.GetTools(mcpClient)

if err != nil {
log.Fatalln("😡", err)
}

// Send request to a LLM with tools suppot
messages := []llm.Message{
{Role: "user", Content: "Fetch this page: https://raw.githubusercontent.com/parakeet-nest/parakeet/main/README.md"},
}

options := llm.SetOptions(map[string]interface{}{
option.Temperature: 0.0,
option.RepeatLastN: 2,
option.RepeatPenalty: 2.0,
})

toolsQuery := llm.Query{
Model: modelWithToolsSupport,
Messages: messages,
Tools: ollamaTools,
Options: options,
Format: "json",
}

answer, err := completion.Chat(ollamaUrl, toolsQuery)
if err != nil {
log.Fatalln("😡", err)
}

// Get the first tool call from the answer (what the LLM wants to do / understand)
toolCall := answer.Message.ToolCalls[0]

// 🖐️ Call the mcp server
fmt.Println("🦙🛠️ 📣 calling:", toolCall.Function.Name, toolCall.Function.Arguments)

mcpResult, err := mcphelpers.CallTool(ctx, mcpClient, toolCall.Function.Name, toolCall.Function.Arguments)
if err != nil {
log.Fatalln("😡", err)
}
// Get the text from the result
contentOfTheWebPage, _ := mcphelpers.GetTextFromResult(mcpResult)

// add this {Role: "user", Content: contentForThePrompt} to the messages
messages = append(messages,
llm.Message{Role: "user", Content: "Make a summary of the below page:"},
llm.Message{Role: "user", Content: contentOfTheWebPage},
)

chatOptions := llm.SetOptions(map[string]interface{}{
option.Temperature: 0.5,
option.RepeatLastN: 2,
option.RepeatPenalty: 3.0,
})

query := llm.Query{
Model: chatModel,
Messages: messages,
Options: chatOptions,
}

_, err = completion.ChatStream(ollamaUrl, query,
func(answer llm.Answer) error {
fmt.Print(answer.Message.Content)
return nil
})

if err != nil {
log.Fatalln("😡", err)
}

}
17 changes: 17 additions & 0 deletions examples/67-mcp/mcp-server/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM golang:1.23.4-alpine AS builder
WORKDIR /app
COPY go.mod .
COPY main.go .

RUN <<EOF
go mod tidy
go build
EOF

FROM curlimages/curl:8.6.0
WORKDIR /app
COPY --from=builder /app/mcp-curl .
ENTRYPOINT ["./mcp-curl"]



49 changes: 49 additions & 0 deletions examples/67-mcp/mcp-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# MCP Curl server

This is a simple server that creates a tool which can fetch web pages using the `curl` command. Let me break it down:

1. The server part:
- Creates an MCP server called "mcp-curl" with version "1.0.0"
- MCP stands for Model Control Protocol - it's a way for AI models to interact with external tools

2. The tool part:
- Creates a tool called "use_curl"
- The tool takes one required parameter: a URL string
- When called, it will fetch the webpage at that URL

3. The handler part:
- The `curlHandler` function is what actually does the work
- It takes the URL from the request
- Uses the system's `curl` command to fetch the webpage
- Returns the webpage content as text
- If anything goes wrong, it returns an error message

4. Server operation:
- Runs over standard input/output (stdio)

This server essentially acts as a bridge between an AI model and the `curl` command, allowing the AI to fetch web content when needed.


## Build it

```bash
docker build -t mcp-curl .
```

## Use it with a MCP client (like Claude.AI Desktop)

```bash
{
"mcpServers": {
"mcp-curl-with-docker" :{
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"mcp-curl"
]
}
}
}
```
7 changes: 7 additions & 0 deletions examples/67-mcp/mcp-server/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module mcp-curl

go 1.23.1

require github.com/mark3labs/mcp-go v0.8.2

require github.com/google/uuid v1.6.0 // indirect
12 changes: 12 additions & 0 deletions examples/67-mcp/mcp-server/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mark3labs/mcp-go v0.8.2 h1:OtqqXlRqjXs6zuMhf1uiuQ2iqBrhMGgLpDeVDUWMKFc=
github.com/mark3labs/mcp-go v0.8.2/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
53 changes: 53 additions & 0 deletions examples/67-mcp/mcp-server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package main

import (
"context"
"fmt"
"os/exec"

"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)

func main() {
// Create MCP server
s := server.NewMCPServer(
"mcp-curl",
"1.0.0",
)

// Add a tool
tool := mcp.NewTool("use_curl",
mcp.WithDescription("fetch this webpage"),
mcp.WithString("url",
mcp.Required(),
mcp.Description("url of the webpage to fetch"),
),
)

// Add a tool handler
s.AddTool(tool, curlHandler)

fmt.Println("🚀 Server started")
// Start the stdio server
if err := server.ServeStdio(s); err != nil {
fmt.Printf("😡 Server error: %v\n", err)
}
fmt.Println("👋 Server stopped")
}

func curlHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {

url, ok := request.Params.Arguments["url"].(string)
if !ok {
return mcp.NewToolResultError("url must be a string"), nil
}
cmd := exec.Command("curl", "-s", url)
output, err := cmd.Output()
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
content := string(output)

return mcp.NewToolResultText(content), nil
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/extism/go-sdk v1.6.1
github.com/go-redis/redis/v8 v8.11.5
github.com/google/uuid v1.6.0
github.com/mark3labs/mcp-go v0.8.3
github.com/sea-monkeys/daphnia v0.0.3
github.com/tetratelabs/wazero v1.8.2
github.com/yuin/goldmark v1.7.8
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mark3labs/mcp-go v0.8.3 h1:IzlyN8BaP4YwUMUDqxOGJhGdZXEDQiAPX43dNPgnzrg=
github.com/mark3labs/mcp-go v0.8.3/go.mod h1:cjMlBU0cv/cj9kjlgmRhoJ5JREdS7YX83xeIG9Ko/jE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
Expand Down
2 changes: 2 additions & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ use (

./examples/65-hyde
./examples/66-structured-outputs
./examples/67-mcp
./examples/67-mcp/mcp-server

examples/90-characters
examples/91-characters
Expand Down
Loading

0 comments on commit b3eea7a

Please sign in to comment.