v / examples / mcp / server.v
210 lines · 197 sloc · 6.69 KB · 7b55539b6e355cd5930ad6213fc49d3c884821dc
Raw
1// Comprehensive MCP server example covering every feature exposed by `vlib/mcp`:
2// tools (with annotations), concrete resources, resource templates, prompts,
3// per-argument completions, RFC 5424 logging, cooperative cancellation,
4// progress notifications, subscriptions, and server-initiated requests.
5//
6// Usage:
7// v run examples/mcp/server.v # stdio transport (default)
8// v run examples/mcp/server.v -- --http # HTTP transport on 127.0.0.1:8080
9// v run examples/mcp/server.v -- --http 127.0.0.1:9000 # HTTP transport on 127.0.0.1:9000
10//
11// Connect a client (e.g. Claude Desktop / Cursor / a custom MCP client) to the
12// command above for stdio, or POST to `http://127.0.0.1:8080/mcp` for HTTP.
13module main
14
15import json
16import mcp
17import os
18import time
19
20struct CountArgs {
21 n int
22}
23
24const supported_languages = ['rust', 'python', 'go', 'v', 'typescript', 'zig']!
25const welcome_text = 'Welcome to the V MCP showcase server.'
26
27fn main() {
28 mut server := mcp.new_server(
29 name: 'v.mcp.showcase'
30 version: '1.0.0'
31 title: 'V MCP Showcase'
32 description: 'Reference server for vlib/mcp covering every capability of the 2025-11-25 spec.'
33 website_url: 'https://vlang.io'
34 icons: [
35 mcp.Icon{
36 src: 'https://vlang.io/img/v-logo.png'
37 mime_type: 'image/png'
38 sizes: ['256x256']
39 },
40 ]
41 instructions: 'Demo server exercising every MCP capability shipped by vlib/mcp.'
42 enable_logging: true
43 // `*` only for the demo; tighten this for real deployments.
44 allowed_origins: ['*']
45 )
46 register_tools(mut server)!
47 register_resources(mut server)!
48 register_prompts(mut server)!
49 register_completions(mut server)!
50
51 // Strip a leading `--` so the same binary works whether launched as
52 // `./v run server.v -- --http :8080` (V's run forwards `--`) or as the
53 // pre-compiled binary `./server --http :8080`.
54 args := os.args[1..].filter(it != '--')
55 if args.len > 0 && args[0] == '--http' {
56 addr := if args.len > 1 { args[1] } else { '127.0.0.1:8080' }
57 eprintln('mcp showcase listening on http://${addr}/mcp')
58 server.serve_http(addr)!
59 } else {
60 server.serve_stdio()!
61 }
62}
63
64fn register_tools(mut server mcp.Server) ! {
65 // `echo` — pure, idempotent, read-only. Demonstrates annotations + icons.
66 server.add_tool(mcp.Tool{
67 name: 'echo'
68 title: 'Echo'
69 description: 'Return the provided text unchanged.'
70 input_schema: '{"type":"object","required":["text"],"properties":{"text":{"type":"string"}}}'
71 icons: [
72 mcp.Icon{
73 src: 'https://vlang.io/img/echo.svg'
74 mime_type: 'image/svg+xml'
75 },
76 ]
77 annotations: mcp.ToolAnnotations{
78 title: 'Echo input'
79 read_only_hint: true
80 idempotent_hint: true
81 open_world_hint: false
82 }
83 }, fn (_ mcp.Context, arguments string) !mcp.ToolResult {
84 // `arguments` is a JSON string the caller may decode. Keep it simple here.
85 return mcp.tool_text_result(arguments)
86 })!
87
88 // `count_to` — long-running tool that reports progress and is cancellable.
89 server.add_tool(mcp.Tool{
90 name: 'count_to'
91 title: 'Counter'
92 description: 'Count up to N with progress notifications. Cooperatively cancellable.'
93 input_schema: '{"type":"object","required":["n"],"properties":{"n":{"type":"integer","minimum":1,"maximum":50}}}'
94 }, fn (ctx mcp.Context, arguments string) !mcp.ToolResult {
95 args := json.decode(CountArgs, arguments) or {
96 return mcp.tool_text_result('invalid arguments: ${err.msg()}')
97 }
98 if args.n < 1 || args.n > 50 {
99 return mcp.tool_text_result('n must be in [1, 50]')
100 }
101 for i in 1 .. args.n + 1 {
102 if ctx.is_cancelled() {
103 return mcp.tool_text_result('cancelled at ${i - 1}')
104 }
105 ctx.notify_progress(f64(i), f64(args.n), 'tick ${i}')
106 time.sleep(50 * time.millisecond)
107 }
108 return mcp.tool_text_result('counted to ${args.n}')
109 })!
110
111 // `delete_record` — destructive; a host can warn the user before invoking.
112 server.add_tool(mcp.Tool{
113 name: 'delete_record'
114 title: 'Delete record (demo)'
115 description: 'Demonstration only — does not delete anything.'
116 annotations: mcp.ToolAnnotations{
117 destructive_hint: true
118 idempotent_hint: false
119 }
120 }, fn (_ mcp.Context, _ string) !mcp.ToolResult {
121 return mcp.tool_text_result('record removed (no-op)')
122 })!
123}
124
125fn register_resources(mut server mcp.Server) ! {
126 server.add_resource(mcp.Resource{
127 uri: 'demo://welcome.txt'
128 name: 'welcome'
129 title: 'Welcome message'
130 description: 'A static welcome string.'
131 mime_type: 'text/plain'
132 size: welcome_text.len
133 annotations: mcp.Annotations{
134 audience: ['user']
135 priority: 0.5
136 }
137 }, fn (_ mcp.Context, uri string) !mcp.ReadResourceResult {
138 return mcp.ReadResourceResult{
139 contents: [
140 mcp.ResourceContents{
141 uri: uri
142 mime_type: 'text/plain'
143 text: welcome_text
144 },
145 ]
146 }
147 })!
148
149 server.add_resource_template(mcp.ResourceTemplate{
150 uri_template: 'demo://greet/{language}'
151 name: 'greet'
152 title: 'Localised greeting'
153 description: 'Returns a greeting in {language}.'
154 mime_type: 'text/plain'
155 })!
156}
157
158fn register_prompts(mut server mcp.Server) ! {
159 server.add_prompt(mcp.Prompt{
160 name: 'review'
161 title: 'Code review'
162 description: 'Ask the assistant to review a snippet in the requested language.'
163 arguments: [
164 mcp.PromptArgument{
165 name: 'language'
166 description: 'Source language of the snippet.'
167 required: true
168 },
169 mcp.PromptArgument{
170 name: 'snippet'
171 description: 'The code to review.'
172 required: true
173 },
174 ]
175 }, fn (_ mcp.Context, arguments string) !mcp.GetPromptResult {
176 return mcp.GetPromptResult{
177 description: 'Code review prompt'
178 messages: [
179 mcp.prompt_text_message('user',
180 'Please review the following code. Arguments: ${arguments}'),
181 ]
182 }
183 })!
184}
185
186fn register_completions(mut server mcp.Server) ! {
187 // Auto-complete the prompt argument `language` against a known list.
188 server.add_completion(mcp.CompletionRef{
189 ref_type: 'ref/prompt'
190 name: 'review'
191 }, 'language', fn (_ mcp.Context, current_value string, _ string) !mcp.CompletionResult {
192 matches := supported_languages.filter(it.starts_with(current_value.to_lower()))
193 return mcp.CompletionResult{
194 values: matches
195 total: matches.len
196 has_more: false
197 }
198 })!
199
200 // Auto-complete the resource template variable `language` similarly.
201 server.add_completion(mcp.CompletionRef{
202 ref_type: 'ref/resource'
203 uri: 'demo://greet/{language}'
204 }, 'language', fn (_ mcp.Context, current_value string, _ string) !mcp.CompletionResult {
205 matches := supported_languages.filter(it.starts_with(current_value.to_lower()))
206 return mcp.CompletionResult{
207 values: matches
208 }
209 })!
210}
211