211 lines
6.7 KiB
V
211 lines
6.7 KiB
V
// Comprehensive MCP server example covering every feature exposed by `vlib/mcp`:
|
|
// tools (with annotations), concrete resources, resource templates, prompts,
|
|
// per-argument completions, RFC 5424 logging, cooperative cancellation,
|
|
// progress notifications, subscriptions, and server-initiated requests.
|
|
//
|
|
// Usage:
|
|
// v run examples/mcp/server.v # stdio transport (default)
|
|
// v run examples/mcp/server.v -- --http # HTTP transport on 127.0.0.1:8080
|
|
// v run examples/mcp/server.v -- --http 127.0.0.1:9000 # HTTP transport on 127.0.0.1:9000
|
|
//
|
|
// Connect a client (e.g. Claude Desktop / Cursor / a custom MCP client) to the
|
|
// command above for stdio, or POST to `http://127.0.0.1:8080/mcp` for HTTP.
|
|
module main
|
|
|
|
import json
|
|
import mcp
|
|
import os
|
|
import time
|
|
|
|
struct CountArgs {
|
|
n int
|
|
}
|
|
|
|
const supported_languages = ['rust', 'python', 'go', 'v', 'typescript', 'zig']!
|
|
const welcome_text = 'Welcome to the V MCP showcase server.'
|
|
|
|
fn main() {
|
|
mut server := mcp.new_server(
|
|
name: 'v.mcp.showcase'
|
|
version: '1.0.0'
|
|
title: 'V MCP Showcase'
|
|
description: 'Reference server for vlib/mcp covering every capability of the 2025-11-25 spec.'
|
|
website_url: 'https://vlang.io'
|
|
icons: [
|
|
mcp.Icon{
|
|
src: 'https://vlang.io/img/v-logo.png'
|
|
mime_type: 'image/png'
|
|
sizes: ['256x256']
|
|
},
|
|
]
|
|
instructions: 'Demo server exercising every MCP capability shipped by vlib/mcp.'
|
|
enable_logging: true
|
|
// `*` only for the demo; tighten this for real deployments.
|
|
allowed_origins: ['*']
|
|
)
|
|
register_tools(mut server)!
|
|
register_resources(mut server)!
|
|
register_prompts(mut server)!
|
|
register_completions(mut server)!
|
|
|
|
// Strip a leading `--` so the same binary works whether launched as
|
|
// `./v run server.v -- --http :8080` (V's run forwards `--`) or as the
|
|
// pre-compiled binary `./server --http :8080`.
|
|
args := os.args[1..].filter(it != '--')
|
|
if args.len > 0 && args[0] == '--http' {
|
|
addr := if args.len > 1 { args[1] } else { '127.0.0.1:8080' }
|
|
eprintln('mcp showcase listening on http://${addr}/mcp')
|
|
server.serve_http(addr)!
|
|
} else {
|
|
server.serve_stdio()!
|
|
}
|
|
}
|
|
|
|
fn register_tools(mut server mcp.Server) ! {
|
|
// `echo` — pure, idempotent, read-only. Demonstrates annotations + icons.
|
|
server.add_tool(mcp.Tool{
|
|
name: 'echo'
|
|
title: 'Echo'
|
|
description: 'Return the provided text unchanged.'
|
|
input_schema: '{"type":"object","required":["text"],"properties":{"text":{"type":"string"}}}'
|
|
icons: [
|
|
mcp.Icon{
|
|
src: 'https://vlang.io/img/echo.svg'
|
|
mime_type: 'image/svg+xml'
|
|
},
|
|
]
|
|
annotations: mcp.ToolAnnotations{
|
|
title: 'Echo input'
|
|
read_only_hint: true
|
|
idempotent_hint: true
|
|
open_world_hint: false
|
|
}
|
|
}, fn (_ mcp.Context, arguments string) !mcp.ToolResult {
|
|
// `arguments` is a JSON string the caller may decode. Keep it simple here.
|
|
return mcp.tool_text_result(arguments)
|
|
})!
|
|
|
|
// `count_to` — long-running tool that reports progress and is cancellable.
|
|
server.add_tool(mcp.Tool{
|
|
name: 'count_to'
|
|
title: 'Counter'
|
|
description: 'Count up to N with progress notifications. Cooperatively cancellable.'
|
|
input_schema: '{"type":"object","required":["n"],"properties":{"n":{"type":"integer","minimum":1,"maximum":50}}}'
|
|
}, fn (ctx mcp.Context, arguments string) !mcp.ToolResult {
|
|
args := json.decode(CountArgs, arguments) or {
|
|
return mcp.tool_text_result('invalid arguments: ${err.msg()}')
|
|
}
|
|
if args.n < 1 || args.n > 50 {
|
|
return mcp.tool_text_result('n must be in [1, 50]')
|
|
}
|
|
for i in 1 .. args.n + 1 {
|
|
if ctx.is_cancelled() {
|
|
return mcp.tool_text_result('cancelled at ${i - 1}')
|
|
}
|
|
ctx.notify_progress(f64(i), f64(args.n), 'tick ${i}')
|
|
time.sleep(50 * time.millisecond)
|
|
}
|
|
return mcp.tool_text_result('counted to ${args.n}')
|
|
})!
|
|
|
|
// `delete_record` — destructive; a host can warn the user before invoking.
|
|
server.add_tool(mcp.Tool{
|
|
name: 'delete_record'
|
|
title: 'Delete record (demo)'
|
|
description: 'Demonstration only — does not delete anything.'
|
|
annotations: mcp.ToolAnnotations{
|
|
destructive_hint: true
|
|
idempotent_hint: false
|
|
}
|
|
}, fn (_ mcp.Context, _ string) !mcp.ToolResult {
|
|
return mcp.tool_text_result('record removed (no-op)')
|
|
})!
|
|
}
|
|
|
|
fn register_resources(mut server mcp.Server) ! {
|
|
server.add_resource(mcp.Resource{
|
|
uri: 'demo://welcome.txt'
|
|
name: 'welcome'
|
|
title: 'Welcome message'
|
|
description: 'A static welcome string.'
|
|
mime_type: 'text/plain'
|
|
size: welcome_text.len
|
|
annotations: mcp.Annotations{
|
|
audience: ['user']
|
|
priority: 0.5
|
|
}
|
|
}, fn (_ mcp.Context, uri string) !mcp.ReadResourceResult {
|
|
return mcp.ReadResourceResult{
|
|
contents: [
|
|
mcp.ResourceContents{
|
|
uri: uri
|
|
mime_type: 'text/plain'
|
|
text: welcome_text
|
|
},
|
|
]
|
|
}
|
|
})!
|
|
|
|
server.add_resource_template(mcp.ResourceTemplate{
|
|
uri_template: 'demo://greet/{language}'
|
|
name: 'greet'
|
|
title: 'Localised greeting'
|
|
description: 'Returns a greeting in {language}.'
|
|
mime_type: 'text/plain'
|
|
})!
|
|
}
|
|
|
|
fn register_prompts(mut server mcp.Server) ! {
|
|
server.add_prompt(mcp.Prompt{
|
|
name: 'review'
|
|
title: 'Code review'
|
|
description: 'Ask the assistant to review a snippet in the requested language.'
|
|
arguments: [
|
|
mcp.PromptArgument{
|
|
name: 'language'
|
|
description: 'Source language of the snippet.'
|
|
required: true
|
|
},
|
|
mcp.PromptArgument{
|
|
name: 'snippet'
|
|
description: 'The code to review.'
|
|
required: true
|
|
},
|
|
]
|
|
}, fn (_ mcp.Context, arguments string) !mcp.GetPromptResult {
|
|
return mcp.GetPromptResult{
|
|
description: 'Code review prompt'
|
|
messages: [
|
|
mcp.prompt_text_message('user',
|
|
'Please review the following code. Arguments: ${arguments}'),
|
|
]
|
|
}
|
|
})!
|
|
}
|
|
|
|
fn register_completions(mut server mcp.Server) ! {
|
|
// Auto-complete the prompt argument `language` against a known list.
|
|
server.add_completion(mcp.CompletionRef{
|
|
ref_type: 'ref/prompt'
|
|
name: 'review'
|
|
}, 'language', fn (_ mcp.Context, current_value string, _ string) !mcp.CompletionResult {
|
|
matches := supported_languages.filter(it.starts_with(current_value.to_lower()))
|
|
return mcp.CompletionResult{
|
|
values: matches
|
|
total: matches.len
|
|
has_more: false
|
|
}
|
|
})!
|
|
|
|
// Auto-complete the resource template variable `language` similarly.
|
|
server.add_completion(mcp.CompletionRef{
|
|
ref_type: 'ref/resource'
|
|
uri: 'demo://greet/{language}'
|
|
}, 'language', fn (_ mcp.Context, current_value string, _ string) !mcp.CompletionResult {
|
|
matches := supported_languages.filter(it.starts_with(current_value.to_lower()))
|
|
return mcp.CompletionResult{
|
|
values: matches
|
|
}
|
|
})!
|
|
}
|