examples: add a server for conformance testing (#681)
Add a server example for the new conformance test suite. Passes all current conformance tests. For #650
This commit is contained in:
parent
4c52f37649
commit
3b42db997c
1 changed files with 775 additions and 0 deletions
775
examples/server/conformance/main.go
Normal file
775
examples/server/conformance/main.go
Normal file
|
|
@ -0,0 +1,775 @@
|
|||
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The conformance server implements features required for MCP conformance testing.
|
||||
// It mirrors the functionality of the TypeScript conformance server at
|
||||
// https://github.com/modelcontextprotocol/conformance/blob/main/examples/servers/typescript/everything-server.ts
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/google/jsonschema-go/jsonschema"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"github.com/yosida95/uritemplate/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
httpAddr = flag.String("http", "", "if set, use streamable HTTP at this address, instead of stdin/stdout")
|
||||
)
|
||||
|
||||
const watchedResourceURI = "test://watched-resource"
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
opts := &mcp.ServerOptions{
|
||||
CompletionHandler: completionHandler,
|
||||
SubscribeHandler: subscribeHandler,
|
||||
UnsubscribeHandler: unsubscribeHandler,
|
||||
}
|
||||
|
||||
server := mcp.NewServer(&mcp.Implementation{
|
||||
Name: "mcp-conformance-test-server",
|
||||
Version: "1.0.0",
|
||||
}, opts)
|
||||
|
||||
// Register server features.
|
||||
registerTools(server)
|
||||
registerResources(server)
|
||||
registerPrompts(server)
|
||||
|
||||
// Start the watched resource auto-update goroutine.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
go watchedResourceUpdater(ctx, server)
|
||||
|
||||
// Serve over stdio, or streamable HTTP if -http is set.
|
||||
if *httpAddr != "" {
|
||||
handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server {
|
||||
return server
|
||||
}, nil)
|
||||
log.Printf("Conformance server listening at %s", *httpAddr)
|
||||
log.Fatal(http.ListenAndServe(*httpAddr, handler))
|
||||
} else {
|
||||
t := &mcp.StdioTransport{}
|
||||
if err := server.Run(ctx, t); err != nil {
|
||||
log.Printf("Server failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// watchedResourceUpdater sends resource update notifications every 3 seconds.
|
||||
func watchedResourceUpdater(ctx context.Context, server *mcp.Server) {
|
||||
ticker := time.NewTicker(3 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
server.ResourceUpdated(ctx, &mcp.ResourceUpdatedNotificationParams{
|
||||
URI: watchedResourceURI,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tools
|
||||
// =============================================================================
|
||||
|
||||
func registerTools(server *mcp.Server) {
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_simple_text",
|
||||
Description: "Tests simple text content response",
|
||||
}, testSimpleTextHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_image_content",
|
||||
Description: "Tests image content response",
|
||||
}, testImageContentHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_audio_content",
|
||||
Description: "Tests audio content response",
|
||||
}, testAudioContentHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_embedded_resource",
|
||||
Description: "Tests embedded resource content response",
|
||||
}, testEmbeddedResourceHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_multiple_content_types",
|
||||
Description: "Tests response with multiple content types (text, image, resource)",
|
||||
}, testMultipleContentTypesHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_tool_with_logging",
|
||||
Description: "Tests tool that emits log messages during execution",
|
||||
}, testToolWithLoggingHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_tool_with_progress",
|
||||
Description: "Tests tool that reports progress notifications",
|
||||
}, testToolWithProgressHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_error_handling",
|
||||
Description: "Tests error response handling",
|
||||
}, testErrorHandlingHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_sampling",
|
||||
Description: "Tests server-initiated sampling (LLM completion request)",
|
||||
}, testSamplingHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_elicitation",
|
||||
Description: "Tests server-initiated elicitation (user input request)",
|
||||
}, testElicitationHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_elicitation_sep1034_defaults",
|
||||
Description: "Tests elicitation with default values per SEP-1034",
|
||||
}, testElicitationDefaultsHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_elicitation_sep1330_enums",
|
||||
Description: "Tests elicitation with enum schema improvements per SEP-1330",
|
||||
}, testElicitationEnumsHandler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "json_schema_2020_12_tool",
|
||||
Description: "Tool with JSON Schema 2020-12 features for conformance testing (SEP-1613)",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"$defs": {
|
||||
"address": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"street": { "type": "string" },
|
||||
"city": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"address": { "$ref": "#/$defs/address" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}`),
|
||||
}, jsonSchema202012Handler)
|
||||
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "test_reconnection",
|
||||
Description: "Tests SSE stream disconnection and client reconnection (SEP-1699). Server will close the stream mid-call and send the result after client reconnects.",
|
||||
}, testReconnectionHandler)
|
||||
}
|
||||
|
||||
// Tool handlers
|
||||
|
||||
func testSimpleTextHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: "This is a simple text response for testing."},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func testImageContentHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.ImageContent{
|
||||
Data: imageData(),
|
||||
MIMEType: "image/png",
|
||||
},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func testAudioContentHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.AudioContent{
|
||||
Data: audioData(),
|
||||
MIMEType: "audio/wav",
|
||||
},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func testEmbeddedResourceHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.EmbeddedResource{
|
||||
Resource: &mcp.ResourceContents{
|
||||
URI: "test://embedded-resource",
|
||||
MIMEType: "text/plain",
|
||||
Text: "This is an embedded resource",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func testMultipleContentTypesHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: "This is text content"},
|
||||
&mcp.ImageContent{
|
||||
Data: imageData(),
|
||||
MIMEType: "image/png",
|
||||
},
|
||||
&mcp.EmbeddedResource{
|
||||
Resource: &mcp.ResourceContents{
|
||||
URI: "test://embedded-in-multiple",
|
||||
MIMEType: "text/plain",
|
||||
Text: "This is an embedded resource",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func testToolWithLoggingHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
// Emit three info-level log messages
|
||||
req.Session.Log(ctx, &mcp.LoggingMessageParams{
|
||||
Level: "info",
|
||||
Data: "Tool execution started",
|
||||
})
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
req.Session.Log(ctx, &mcp.LoggingMessageParams{
|
||||
Level: "info",
|
||||
Data: "Tool processing data",
|
||||
})
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
req.Session.Log(ctx, &mcp.LoggingMessageParams{
|
||||
Level: "info",
|
||||
Data: "Tool execution completed",
|
||||
})
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: "Tool with logging executed successfully"},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func testToolWithProgressHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
// Get progress token from the request if provided
|
||||
progressToken := req.Params.GetProgressToken()
|
||||
|
||||
// Send three progress notifications (0%, 50%, 100%)
|
||||
total := 100.0
|
||||
steps := []float64{0, 50, 100}
|
||||
for _, progress := range steps {
|
||||
req.Session.NotifyProgress(ctx, &mcp.ProgressNotificationParams{
|
||||
ProgressToken: progressToken,
|
||||
Progress: progress,
|
||||
Total: total,
|
||||
Message: fmt.Sprintf("Completed step %.0f of %.0f", progress, total),
|
||||
})
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Return the progress token value as the response (matching TypeScript behavior)
|
||||
tokenStr := fmt.Sprintf("%v", progressToken)
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: tokenStr},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func testErrorHandlingHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
return nil, nil, errors.New("this tool intentionally returns an error for testing")
|
||||
}
|
||||
|
||||
type samplingInput struct {
|
||||
Prompt string `json:"prompt" jsonschema:"The prompt to send to the LLM"`
|
||||
}
|
||||
|
||||
func testSamplingHandler(ctx context.Context, req *mcp.CallToolRequest, input samplingInput) (*mcp.CallToolResult, any, error) {
|
||||
// Request LLM completion from the client
|
||||
result, err := req.Session.CreateMessage(ctx, &mcp.CreateMessageParams{
|
||||
Messages: []*mcp.SamplingMessage{
|
||||
{
|
||||
Role: "user",
|
||||
Content: &mcp.TextContent{
|
||||
Text: input.Prompt,
|
||||
},
|
||||
},
|
||||
},
|
||||
MaxTokens: 100,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("sampling failed: %w", err)
|
||||
}
|
||||
|
||||
// Extract the text response from the result
|
||||
var responseText string
|
||||
if tc, ok := result.Content.(*mcp.TextContent); ok {
|
||||
responseText = tc.Text
|
||||
} else {
|
||||
responseText = "(non-text response)"
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: fmt.Sprintf("LLM response: %s", responseText)},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
type elicitationInput struct {
|
||||
Message string `json:"message" jsonschema:"The message to show the user"`
|
||||
}
|
||||
|
||||
func testElicitationHandler(ctx context.Context, req *mcp.CallToolRequest, input elicitationInput) (*mcp.CallToolResult, any, error) {
|
||||
result, err := req.Session.Elicit(ctx, &mcp.ElicitParams{
|
||||
Message: input.Message,
|
||||
RequestedSchema: &jsonschema.Schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*jsonschema.Schema{
|
||||
"username": {
|
||||
Type: "string",
|
||||
Description: "Your preferred username",
|
||||
},
|
||||
},
|
||||
Required: []string{"username"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("elicitation failed: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: fmt.Sprintf("Elicitation result: action=%s, content=%v", result.Action, result.Content)},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func testElicitationDefaultsHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
result, err := req.Session.Elicit(ctx, &mcp.ElicitParams{
|
||||
Message: "Test defaults for primitives",
|
||||
RequestedSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"name": map[string]any{
|
||||
"type": "string",
|
||||
"description": "User name",
|
||||
"default": "John Doe",
|
||||
},
|
||||
"age": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "User age",
|
||||
"default": 30,
|
||||
},
|
||||
"score": map[string]any{
|
||||
"type": "number",
|
||||
"description": "User score",
|
||||
"default": 95.5,
|
||||
},
|
||||
"status": map[string]any{
|
||||
"type": "string",
|
||||
"description": "User status",
|
||||
"enum": []string{"active", "inactive", "pending"},
|
||||
"default": "active",
|
||||
},
|
||||
"verified": map[string]any{
|
||||
"type": "boolean",
|
||||
"description": "Verification status",
|
||||
"default": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("elicitation failed: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: fmt.Sprintf("Elicitation result: action=%s, content=%v", result.Action, result.Content)},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func testElicitationEnumsHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
result, err := req.Session.Elicit(ctx, &mcp.ElicitParams{
|
||||
Message: "Test enum schemas",
|
||||
RequestedSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
// Basic enum without titles
|
||||
"untitledSingle": map[string]any{
|
||||
"type": "string",
|
||||
"enum": []string{"option1", "option2", "option3"},
|
||||
},
|
||||
// Enum with titles using oneOf
|
||||
"titledSingle": map[string]any{
|
||||
"type": "string",
|
||||
"oneOf": []map[string]any{
|
||||
{"const": "value1", "title": "First Option"},
|
||||
{"const": "value2", "title": "Second Option"},
|
||||
{"const": "value3", "title": "Third Option"},
|
||||
},
|
||||
},
|
||||
// Legacy enum with enumNames
|
||||
"legacyEnum": map[string]any{
|
||||
"type": "string",
|
||||
"enum": []string{"opt1", "opt2", "opt3"},
|
||||
"enumNames": []string{"Option One", "Option Two", "Option Three"},
|
||||
},
|
||||
// Multi-select without titles
|
||||
"untitledMulti": map[string]any{
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 3,
|
||||
"items": map[string]any{
|
||||
"type": "string",
|
||||
"enum": []string{"option1", "option2", "option3"},
|
||||
},
|
||||
},
|
||||
// Multi-select with titles using anyOf
|
||||
"titledMulti": map[string]any{
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 3,
|
||||
"items": map[string]any{
|
||||
"type": "string",
|
||||
"anyOf": []map[string]any{
|
||||
{"const": "value1", "title": "First Option"},
|
||||
{"const": "value2", "title": "Second Option"},
|
||||
{"const": "value3", "title": "Third Option"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("elicitation failed: %w", err)
|
||||
}
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: fmt.Sprintf("Elicitation result: action=%s, content=%v", result.Action, result.Content)},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
type jsonSchemaAddress struct {
|
||||
Street string `json:"street"`
|
||||
City string `json:"city"`
|
||||
}
|
||||
|
||||
type jsonSchemaInput struct {
|
||||
Name string `json:"name"`
|
||||
Address *jsonSchemaAddress `json:"address"`
|
||||
}
|
||||
|
||||
func jsonSchema202012Handler(ctx context.Context, req *mcp.CallToolRequest, input jsonSchemaInput) (*mcp.CallToolResult, any, error) {
|
||||
// Echo back the arguments received
|
||||
var addressStr string
|
||||
if input.Address != nil {
|
||||
addressStr = fmt.Sprintf("{street: %q, city: %q}", input.Address.Street, input.Address.City)
|
||||
} else {
|
||||
addressStr = "nil"
|
||||
}
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{
|
||||
Text: fmt.Sprintf("Received: name=%q, address=%s", input.Name, addressStr),
|
||||
},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
func testReconnectionHandler(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||
// Close the SSE stream to trigger client reconnection (SEP-1699)
|
||||
if req.Extra != nil && req.Extra.CloseStream != nil {
|
||||
req.Extra.CloseStream(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Wait for client to reconnect
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
return &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{
|
||||
Text: "Reconnection test completed successfully. If you received this, the client properly reconnected after stream closure.",
|
||||
},
|
||||
},
|
||||
}, nil, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Resources
|
||||
// =============================================================================
|
||||
|
||||
func registerResources(server *mcp.Server) {
|
||||
server.AddResource(&mcp.Resource{
|
||||
Name: "static-text",
|
||||
Description: "A static text resource for testing",
|
||||
MIMEType: "text/plain",
|
||||
URI: "test://static-text",
|
||||
}, staticTextHandler)
|
||||
|
||||
server.AddResource(&mcp.Resource{
|
||||
Name: "static-binary",
|
||||
Description: "A static binary resource (image) for testing",
|
||||
MIMEType: "image/png",
|
||||
URI: "test://static-binary",
|
||||
}, staticBinaryHandler)
|
||||
|
||||
server.AddResourceTemplate(&mcp.ResourceTemplate{
|
||||
Name: "template",
|
||||
Description: "A resource template with parameter substitution",
|
||||
MIMEType: "application/json",
|
||||
URITemplate: "test://template/{id}/data",
|
||||
}, templateResourceHandler)
|
||||
|
||||
server.AddResource(&mcp.Resource{
|
||||
Name: "watched-resource",
|
||||
Description: "A resource that auto-updates every 3 seconds",
|
||||
MIMEType: "text/plain",
|
||||
URI: watchedResourceURI,
|
||||
}, watchedResourceHandler)
|
||||
}
|
||||
|
||||
func staticTextHandler(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
|
||||
return &mcp.ReadResourceResult{
|
||||
Contents: []*mcp.ResourceContents{
|
||||
{
|
||||
URI: req.Params.URI,
|
||||
MIMEType: "text/plain",
|
||||
Text: "This is the content of the static text resource.",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func staticBinaryHandler(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
|
||||
return &mcp.ReadResourceResult{
|
||||
Contents: []*mcp.ResourceContents{
|
||||
{
|
||||
URI: req.Params.URI,
|
||||
MIMEType: "image/png",
|
||||
Blob: imageData(),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// templatePattern is the compiled URI template for the template resource.
|
||||
var templatePattern = uritemplate.MustNew("test://template/{id}/data")
|
||||
|
||||
func templateResourceHandler(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
|
||||
// Extract the ID from the URI using the template pattern
|
||||
uri := req.Params.URI
|
||||
match := templatePattern.Regexp().FindStringSubmatch(uri)
|
||||
id := ""
|
||||
if len(match) > 1 {
|
||||
id = match[1]
|
||||
}
|
||||
|
||||
jsonContent := fmt.Sprintf(`{"id": "%s", "templateTest": true, "data": "Data for ID: %s"}`, id, id)
|
||||
return &mcp.ReadResourceResult{
|
||||
Contents: []*mcp.ResourceContents{
|
||||
{
|
||||
URI: uri,
|
||||
MIMEType: "application/json",
|
||||
Text: jsonContent,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func watchedResourceHandler(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
|
||||
return &mcp.ReadResourceResult{
|
||||
Contents: []*mcp.ResourceContents{
|
||||
{
|
||||
URI: req.Params.URI,
|
||||
MIMEType: "text/plain",
|
||||
Text: "Watched resource content",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Prompts
|
||||
// =============================================================================
|
||||
|
||||
func registerPrompts(server *mcp.Server) {
|
||||
server.AddPrompt(&mcp.Prompt{
|
||||
Name: "test_simple_prompt",
|
||||
Title: "Simple Test Prompt",
|
||||
Description: "A simple prompt without arguments",
|
||||
}, simplePromptHandler)
|
||||
|
||||
server.AddPrompt(&mcp.Prompt{
|
||||
Name: "test_prompt_with_arguments",
|
||||
Title: "Prompt With Arguments",
|
||||
Description: "A prompt with required arguments",
|
||||
Arguments: []*mcp.PromptArgument{
|
||||
{Name: "arg1", Description: "First test argument", Required: true},
|
||||
{Name: "arg2", Description: "Second test argument", Required: true},
|
||||
},
|
||||
}, promptWithArgumentsHandler)
|
||||
|
||||
server.AddPrompt(&mcp.Prompt{
|
||||
Name: "test_prompt_with_embedded_resource",
|
||||
Title: "Prompt With Embedded Resource",
|
||||
Description: "A prompt that includes an embedded resource",
|
||||
Arguments: []*mcp.PromptArgument{
|
||||
{Name: "resourceUri", Description: "URI of the resource to embed", Required: true},
|
||||
},
|
||||
}, promptWithEmbeddedResourceHandler)
|
||||
|
||||
server.AddPrompt(&mcp.Prompt{
|
||||
Name: "test_prompt_with_image",
|
||||
Title: "Prompt With Image",
|
||||
Description: "A prompt that includes image content",
|
||||
}, promptWithImageHandler)
|
||||
}
|
||||
|
||||
func simplePromptHandler(ctx context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
|
||||
return &mcp.GetPromptResult{
|
||||
Description: "A simple test prompt",
|
||||
Messages: []*mcp.PromptMessage{
|
||||
{
|
||||
Role: "user",
|
||||
Content: &mcp.TextContent{Text: "This is a simple prompt for testing."},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func promptWithArgumentsHandler(ctx context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
|
||||
arg1 := req.Params.Arguments["arg1"]
|
||||
arg2 := req.Params.Arguments["arg2"]
|
||||
|
||||
return &mcp.GetPromptResult{
|
||||
Description: "A prompt with arguments",
|
||||
Messages: []*mcp.PromptMessage{
|
||||
{
|
||||
Role: "user",
|
||||
Content: &mcp.TextContent{
|
||||
Text: fmt.Sprintf("Prompt with arguments: arg1='%s', arg2='%s'", arg1, arg2),
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func promptWithEmbeddedResourceHandler(ctx context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
|
||||
resourceUri := req.Params.Arguments["resourceUri"]
|
||||
|
||||
return &mcp.GetPromptResult{
|
||||
Description: "A prompt with an embedded resource",
|
||||
Messages: []*mcp.PromptMessage{
|
||||
{
|
||||
Role: "user",
|
||||
Content: &mcp.EmbeddedResource{
|
||||
Resource: &mcp.ResourceContents{
|
||||
URI: resourceUri,
|
||||
MIMEType: "text/plain",
|
||||
Text: "Embedded resource content for testing.",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: "user",
|
||||
Content: &mcp.TextContent{Text: "Please process the embedded resource above."},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func promptWithImageHandler(ctx context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
|
||||
return &mcp.GetPromptResult{
|
||||
Description: "A prompt with an image",
|
||||
Messages: []*mcp.PromptMessage{
|
||||
{
|
||||
Role: "user",
|
||||
Content: &mcp.ImageContent{
|
||||
Data: imageData(),
|
||||
MIMEType: "image/png",
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: "user",
|
||||
Content: &mcp.TextContent{Text: "Please analyze the image above."},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Server handlers
|
||||
// =============================================================================
|
||||
|
||||
func completionHandler(ctx context.Context, req *mcp.CompleteRequest) (*mcp.CompleteResult, error) {
|
||||
// Return empty completion - just acknowledging the capability
|
||||
return &mcp.CompleteResult{
|
||||
Completion: mcp.CompletionResultDetails{
|
||||
Values: []string{},
|
||||
Total: 0,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func subscribeHandler(ctx context.Context, req *mcp.SubscribeRequest) error {
|
||||
// The SDK handles subscription tracking internally via Server.ResourceUpdated()
|
||||
return nil
|
||||
}
|
||||
|
||||
func unsubscribeHandler(ctx context.Context, req *mcp.UnsubscribeRequest) error {
|
||||
// The SDK handles subscription tracking internally
|
||||
return nil
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Helper functions
|
||||
// =============================================================================
|
||||
|
||||
// Base64-encoded minimal test files, copied from the typescript conformance example.
|
||||
const (
|
||||
// Minimal 1x1 red PNG image
|
||||
testImageBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="
|
||||
// Minimal WAV audio file (silence)
|
||||
testAudioBase64 = "UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQIAAAA="
|
||||
)
|
||||
|
||||
func imageData() []byte {
|
||||
data, err := base64.StdEncoding.DecodeString(testImageBase64)
|
||||
if err != nil {
|
||||
panic("invalid testImageBase64: " + err.Error())
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func audioData() []byte {
|
||||
data, err := base64.StdEncoding.DecodeString(testAudioBase64)
|
||||
if err != nil {
|
||||
panic("invalid testAudioBase64: " + err.Error())
|
||||
}
|
||||
return data
|
||||
}
|
||||
Loading…
Reference in a new issue