| 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. |
| 13 | module main |
| 14 | |
| 15 | import json |
| 16 | import mcp |
| 17 | import os |
| 18 | import time |
| 19 | |
| 20 | struct CountArgs { |
| 21 | n int |
| 22 | } |
| 23 | |
| 24 | const supported_languages = ['rust', 'python', 'go', 'v', 'typescript', 'zig']! |
| 25 | const welcome_text = 'Welcome to the V MCP showcase server.' |
| 26 | |
| 27 | fn 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 | |
| 64 | fn 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 | |
| 125 | fn 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 | |
| 158 | fn 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 | |
| 186 | fn 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 | |