Files
agila_open_tool/tools/v/examples/mcp/server.v
T

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
}
})!
}