Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,30 @@ func main() {
),
)

// Add the tool to the server with a handler
// Example of a tool with array parameter
arrayExampleTool := tools.NewTool("array_example",
tools.WithDescription("Example tool with array parameter"),
tools.WithArray("values",
tools.Description("Array of string values"),
tools.Required(),
tools.Items(map[string]interface{}{
"type": "string",
}),
),
)

// Add the tools to the server with handlers
ctx := context.Background()
err := mcpServer.AddTool(ctx, echoTool, handleEcho)
if err != nil {
logger.Fatalf("Error adding tool: %v", err)
}

err = mcpServer.AddTool(ctx, arrayExampleTool, handleArrayExample)
if err != nil {
logger.Fatalf("Error adding array example tool: %v", err)
}

// Write server status to stderr instead of stdout to maintain clean JSON protocol
fmt.Fprintf(os.Stderr, "Starting Echo Server...\n")
fmt.Fprintf(os.Stderr, "Send JSON-RPC messages via stdin to interact with the server.\n")
Expand Down Expand Up @@ -126,6 +143,26 @@ func handleEcho(ctx context.Context, request server.ToolCallRequest) (interface{
},
}, nil
}

// Array example tool handler
func handleArrayExample(ctx context.Context, request server.ToolCallRequest) (interface{}, error) {
// Extract the values parameter
values, ok := request.Parameters["values"].([]interface{})
if !ok {
return nil, fmt.Errorf("missing or invalid 'values' parameter")
}

// Convert values to string array
stringValues := make([]string, len(values))
for i, v := range values {
stringValues[i] = v.(string)
}

// Return the array response in the format expected by the MCP protocol
return map[string]interface{}{
"content": stringValues,
}, nil
}
```

## What is MCP?
Expand Down
Binary file added agent-sdk-test
Binary file not shown.
Binary file added array-parameter-test
Binary file not shown.
Binary file modified bin/multi-protocol-server
Binary file not shown.
Binary file modified bin/sse-server
Binary file not shown.
Binary file modified bin/stdio-server
Binary file not shown.
105 changes: 105 additions & 0 deletions examples/agent-sdk-test/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package main

import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"

"github.com/FreePeak/cortex/pkg/server"
"github.com/FreePeak/cortex/pkg/tools"
)

func main() {
// Create a logger that writes to stderr
logger := log.New(os.Stderr, "[agent-sdk-test] ", log.LstdFlags)

// Create the server
mcpServer := server.NewMCPServer("Agent SDK Test", "1.0.0", logger)

// Configure HTTP address
mcpServer.SetAddress(":9095")

// Create a tool with array parameter (compatible with OpenAI Agent SDK)
queryTool := tools.NewTool("query_database",
tools.WithDescription("Execute SQL query on a database"),
tools.WithString("query",
tools.Description("SQL query to execute"),
tools.Required(),
),
tools.WithArray("params",
tools.Description("Query parameters"),
tools.Items(map[string]interface{}{
"type": "string",
}),
),
)

// Add tool to the server
ctx := context.Background()
err := mcpServer.AddTool(ctx, queryTool, handleQuery)
if err != nil {
logger.Fatalf("Error adding tool: %v", err)
}

// Start HTTP server in a goroutine
go func() {
logger.Printf("Starting Agent SDK Test server on %s", mcpServer.GetAddress())
logger.Printf("Use the following URL in your OpenAI Agent SDK configuration: http://localhost:9095/sse")

if err := mcpServer.ServeHTTP(); err != nil {
logger.Fatalf("HTTP server error: %v", err)
}
}()

// Wait for shutdown signal
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop

// Shutdown gracefully
logger.Println("Shutting down server...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

if err := mcpServer.Shutdown(shutdownCtx); err != nil {
logger.Fatalf("Server shutdown error: %v", err)
}

logger.Println("Server shutdown complete")
}

// Handler for the query tool
func handleQuery(ctx context.Context, request server.ToolCallRequest) (interface{}, error) {
// Extract the query parameter
query, ok := request.Parameters["query"].(string)
if !ok {
return nil, fmt.Errorf("missing or invalid 'query' parameter")
}

// Get optional parameters
var params []interface{}
if paramsVal, ok := request.Parameters["params"]; ok {
params, _ = paramsVal.([]interface{})
}

// In a real implementation, you would execute the query with the parameters
// For this example, we'll just return mock data

// Log the request
log.Printf("Query received: %s", query)
log.Printf("Parameters: %v", params)

// Return a mock response
return map[string]interface{}{
"content": []map[string]interface{}{
{
"type": "text",
"text": fmt.Sprintf("Executed query: %s\nParameters: %v\n\nID\tName\tValue\n1\tItem1\t100\n2\tItem2\t200", query, params),
},
},
}, nil
}
109 changes: 109 additions & 0 deletions examples/array-parameter-test/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package main

import (
"context"
"encoding/json"
"fmt"
"log"
"os"

"github.com/FreePeak/cortex/pkg/server"
"github.com/FreePeak/cortex/pkg/tools"
"github.com/FreePeak/cortex/pkg/types"
)

func main() {
// Create a logger that writes to stderr
logger := log.New(os.Stderr, "[array-test] ", log.LstdFlags)

// Create the server
mcpServer := server.NewMCPServer("Array Parameter Test", "1.0.0", logger)

// Create a tool with array parameter
arrayTool := tools.NewTool("array_test",
tools.WithDescription("Test tool with array parameter"),
tools.WithArray("string_array",
tools.Description("Array of strings"),
tools.Required(),
tools.Items(map[string]interface{}{
"type": "string",
}),
),
tools.WithArray("number_array",
tools.Description("Array of numbers"),
tools.Items(map[string]interface{}{
"type": "number",
}),
),
)

// Add the tool to the server
ctx := context.Background()
err := mcpServer.AddTool(ctx, arrayTool, handleArrayTest)
if err != nil {
logger.Fatalf("Error adding tool: %v", err)
}

// Print tool schema for debugging
printToolSchema(arrayTool)

// Write server status to stderr
fmt.Fprintf(os.Stderr, "Starting Array Parameter Test Server...\n")
fmt.Fprintf(os.Stderr, "Send JSON-RPC messages via stdin to interact with the server.\n")
fmt.Fprintf(os.Stderr, `Try: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"array_test","parameters":{"string_array":["a","b","c"]}}}\n`)

// Serve over stdio
if err := mcpServer.ServeStdio(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

// Handler for the array test tool
func handleArrayTest(ctx context.Context, request server.ToolCallRequest) (interface{}, error) {
// Extract the string array parameter
stringArray, ok := request.Parameters["string_array"].([]interface{})
if !ok {
return nil, fmt.Errorf("missing or invalid 'string_array' parameter")
}

// Get the optional number array parameter
var numberArray []interface{}
if val, ok := request.Parameters["number_array"]; ok {
numberArray, _ = val.([]interface{})
}

// Return the arrays in the response
return map[string]interface{}{
"content": []map[string]interface{}{
{
"type": "text",
"text": fmt.Sprintf("Received string array: %v\nReceived number array: %v", stringArray, numberArray),
},
},
}, nil
}

// Print the tool schema
func printToolSchema(tool *types.Tool) {
schema := map[string]interface{}{
"type": "object",
"properties": make(map[string]interface{}),
}

for _, param := range tool.Parameters {
paramSchema := map[string]interface{}{
"type": param.Type,
"description": param.Description,
}

if param.Type == "array" && param.Items != nil {
paramSchema["items"] = param.Items
}

schema["properties"].(map[string]interface{})[param.Name] = paramSchema
}

schemaJSON, _ := json.MarshalIndent(schema, "", " ")
fmt.Fprintf(os.Stderr, "Tool schema:\n%s\n", schemaJSON)
}
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
1 change: 1 addition & 0 deletions internal/domain/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type ToolParameter struct {
Description string
Type string
Required bool
Items map[string]interface{}
}

// ToolCall represents a request to execute a tool.
Expand Down
7 changes: 6 additions & 1 deletion internal/infrastructure/logging/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ type Config struct {
Development bool
OutputPaths []string
InitialFields Fields
LogToFile bool
LogDir string
}

// DefaultConfig returns a default configuration for the logger
Expand Down Expand Up @@ -105,7 +107,10 @@ func getStdioSafeOutputs() []string {
// Create a log file in the logs directory
logsDir := "logs"
if _, err := os.Stat(logsDir); os.IsNotExist(err) {
os.MkdirAll(logsDir, 0755)
if err := os.MkdirAll(logsDir, 0755); err != nil {
// If we can't create the directory, just use stderr
return []string{"stderr"}
}
}

// Create a timestamped log file
Expand Down
6 changes: 6 additions & 0 deletions internal/interfaces/rest/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,12 @@ func (s *MCPServer) processToolsList(ctx context.Context, request domain.JSONRPC
"type": param.Type,
"description": param.Description,
}

// Add items schema for array parameters
if param.Type == "array" && param.Items != nil {
paramObj["items"] = param.Items
}

properties[param.Name] = paramObj

if param.Required {
Expand Down
6 changes: 6 additions & 0 deletions internal/interfaces/stdio/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,12 @@ func (p *MessageProcessor) handleToolsList(ctx context.Context, params interface
"type": param.Type,
"description": param.Description,
}

// Add items schema for array parameters
if param.Type == "array" && param.Items != nil {
paramObj["items"] = param.Items
}

properties[param.Name] = paramObj

if param.Required {
Expand Down
3 changes: 3 additions & 0 deletions pkg/builder/server_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ func (a *toolRepositoryAdapter) GetTool(ctx context.Context, name string) (*inte
Description: param.Description,
Type: param.Type,
Required: param.Required,
Items: param.Items,
}
}

Expand All @@ -249,6 +250,7 @@ func (a *toolRepositoryAdapter) ListTools(ctx context.Context) ([]*internalDomai
Description: param.Description,
Type: param.Type,
Required: param.Required,
Items: param.Items,
}
}
}
Expand All @@ -269,6 +271,7 @@ func (a *toolRepositoryAdapter) AddTool(ctx context.Context, tool *internalDomai
Description: param.Description,
Type: param.Type,
Required: param.Required,
Items: param.Items,
}
}

Expand Down
2 changes: 2 additions & 0 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ func (s *MCPServer) RegisterProvider(ctx context.Context, provider plugin.Provid
Description: param.Description,
Type: param.Type,
Required: param.Required,
Items: param.Items,
}
}

Expand Down Expand Up @@ -292,6 +293,7 @@ func convertToInternalTool(tool *types.Tool) *domain.Tool {
Description: param.Description,
Type: param.Type,
Required: param.Required,
Items: param.Items,
}
}

Expand Down
7 changes: 7 additions & 0 deletions pkg/tools/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ func Required() ParameterOption {
}
}

// Items sets the schema for items in an array parameter.
func Items(schema map[string]interface{}) ParameterOption {
return func(p *types.ToolParameter) {
p.Items = schema
}
}

// Type functions for creating parameters

// WithString adds a string parameter to a tool.
Expand Down
Loading
Loading