vector_mcp 0.3.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/README.md +147 -337
  4. data/lib/vector_mcp/definitions.rb +30 -0
  5. data/lib/vector_mcp/handlers/core.rb +78 -81
  6. data/lib/vector_mcp/image_util.rb +34 -11
  7. data/lib/vector_mcp/middleware/anonymizer.rb +186 -0
  8. data/lib/vector_mcp/middleware/base.rb +1 -5
  9. data/lib/vector_mcp/middleware/context.rb +11 -1
  10. data/lib/vector_mcp/middleware/hook.rb +7 -24
  11. data/lib/vector_mcp/middleware.rb +26 -9
  12. data/lib/vector_mcp/rails/tool.rb +85 -0
  13. data/lib/vector_mcp/request_context.rb +1 -1
  14. data/lib/vector_mcp/security/auth_manager.rb +12 -13
  15. data/lib/vector_mcp/security/auth_result.rb +33 -0
  16. data/lib/vector_mcp/security/authorization.rb +5 -9
  17. data/lib/vector_mcp/security/middleware.rb +2 -2
  18. data/lib/vector_mcp/security/session_context.rb +11 -27
  19. data/lib/vector_mcp/security/strategies/api_key.rb +1 -5
  20. data/lib/vector_mcp/security/strategies/custom.rb +10 -37
  21. data/lib/vector_mcp/security/strategies/jwt_token.rb +1 -10
  22. data/lib/vector_mcp/server/capabilities.rb +22 -32
  23. data/lib/vector_mcp/server/message_handling.rb +21 -14
  24. data/lib/vector_mcp/server/registry.rb +102 -120
  25. data/lib/vector_mcp/server.rb +98 -57
  26. data/lib/vector_mcp/session.rb +5 -3
  27. data/lib/vector_mcp/token_store.rb +80 -0
  28. data/lib/vector_mcp/tool.rb +221 -0
  29. data/lib/vector_mcp/transport/base_session_manager.rb +1 -17
  30. data/lib/vector_mcp/transport/http_stream/event_store.rb +29 -17
  31. data/lib/vector_mcp/transport/http_stream/session_manager.rb +41 -36
  32. data/lib/vector_mcp/transport/http_stream/stream_handler.rb +132 -47
  33. data/lib/vector_mcp/transport/http_stream.rb +242 -124
  34. data/lib/vector_mcp/util/token_sweeper.rb +74 -0
  35. data/lib/vector_mcp/version.rb +1 -1
  36. data/lib/vector_mcp.rb +8 -8
  37. metadata +8 -10
  38. data/lib/vector_mcp/transport/sse/client_connection.rb +0 -113
  39. data/lib/vector_mcp/transport/sse/message_handler.rb +0 -166
  40. data/lib/vector_mcp/transport/sse/puma_config.rb +0 -77
  41. data/lib/vector_mcp/transport/sse/stream_manager.rb +0 -92
  42. data/lib/vector_mcp/transport/sse.rb +0 -377
  43. data/lib/vector_mcp/transport/sse_session_manager.rb +0 -188
  44. data/lib/vector_mcp/transport/stdio.rb +0 -473
  45. data/lib/vector_mcp/transport/stdio_session_manager.rb +0 -181
data/README.md CHANGED
@@ -6,441 +6,251 @@
6
6
  [![Maintainability](https://qlty.sh/badges/fdb143b3-148a-4a86-8e3b-4ccebe993528/maintainability.svg)](https://qlty.sh/gh/sergiobayona/projects/vector_mcp)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
8
 
9
- VectorMCP is a Ruby gem implementing the Model Context Protocol (MCP) server-side specification. It provides a framework for creating MCP servers that expose tools, resources, prompts, and roots to LLM clients.
9
+ VectorMCP is a Ruby implementation of the Model Context Protocol (MCP) server-side specification. It gives you a framework for exposing tools, resources, prompts, roots, sampling, middleware, and security over the MCP streamable HTTP transport.
10
10
 
11
- ## Why VectorMCP?
11
+ ## Highlights
12
12
 
13
- - **🛡️ Security-First**: Built-in input validation and schema checking prevent injection attacks
14
- - **⚡ Production-Ready**: Robust error handling, comprehensive test suite, and proven reliability
15
- - **🔌 Multiple Transports**: stdio for CLI tools, SSE for web applications
16
- - **📦 Zero Configuration**: Works out of the box with sensible defaults
17
- - **🔄 Fully Compatible**: Implements the complete MCP specification
13
+ - Streamable HTTP is the built-in transport, with session management, resumability, and MCP 2025-11-25 compliance
14
+ - Class-based tools via `VectorMCP::Tool`, plus the original block-based `register_tool` API
15
+ - Rack and Rails mounting through `server.rack_app`
16
+ - Opt-in authentication and authorization, structured logging, and middleware hooks
17
+ - Image-aware tools/resources/prompts, roots, and server-initiated sampling
18
+ - Token-based field anonymization middleware to keep sensitive values out of LLM context
18
19
 
19
- ## Quick Start
20
+ ## Requirements
21
+
22
+ - Ruby 3.2+
23
+
24
+ ## Installation
20
25
 
21
26
  ```bash
22
27
  gem install vector_mcp
23
28
  ```
24
29
 
25
30
  ```ruby
26
- require 'vector_mcp'
31
+ gem "vector_mcp"
32
+ ```
27
33
 
28
- # Create a server
29
- server = VectorMCP.new(name: 'MyApp', version: '1.0.0')
34
+ ## Quick Start
30
35
 
31
- # Add a tool
36
+ ```ruby
37
+ require "vector_mcp"
38
+
39
+ class Greet < VectorMCP::Tool
40
+ description "Say hello to someone"
41
+ param :name, type: :string, desc: "Name to greet", required: true
42
+
43
+ def call(args, _session)
44
+ "Hello, #{args["name"]}!"
45
+ end
46
+ end
47
+
48
+ server = VectorMCP::Server.new(name: "MyApp", version: "1.0.0")
49
+ server.register(Greet)
50
+ server.run(port: 8080)
51
+ ```
52
+
53
+ The class-based DSL is optional. The existing block-based API still works:
54
+
55
+ ```ruby
32
56
  server.register_tool(
33
- name: 'greet',
34
- description: 'Says hello to someone',
57
+ name: "echo",
58
+ description: "Echo back the supplied text",
35
59
  input_schema: {
36
- type: 'object',
37
- properties: { name: { type: 'string' } },
38
- required: ['name']
60
+ type: "object",
61
+ properties: { text: { type: "string" } },
62
+ required: ["text"]
39
63
  }
40
- ) { |args| "Hello, #{args['name']}!" }
41
-
42
- # Start the server
43
- server.run # Uses stdio transport by default
64
+ ) { |args| args["text"] }
44
65
  ```
45
66
 
46
- **That's it!** Your MCP server is ready to connect with Claude Desktop, custom clients, or any MCP-compatible application.
67
+ ## Rack and Rails
68
+
69
+ VectorMCP can run as a standalone HTTP server or be mounted inside an existing Rack app:
47
70
 
48
- ## Transport Options
71
+ ```ruby
72
+ require "vector_mcp"
73
+
74
+ server = VectorMCP::Server.new(name: "MyApp", version: "1.0.0")
75
+ server.register(Greet)
49
76
 
50
- ### Command Line Tools (stdio)
77
+ MCP_APP = server.rack_app
78
+ ```
51
79
 
52
- Perfect for desktop applications and process-based integrations:
80
+ In Rails, mount it in `config/routes.rb`:
53
81
 
54
82
  ```ruby
55
- server.run # Default: stdio transport
83
+ mount MCP_APP => "/mcp"
56
84
  ```
57
85
 
58
- ### Web Applications (HTTP + SSE)
59
-
60
- Ideal for web apps and browser-based clients:
86
+ For ActiveRecord-backed tools, opt into `VectorMCP::Rails::Tool`:
61
87
 
62
88
  ```ruby
63
- server.run(transport: :sse, port: 8080)
64
- ```
89
+ require "vector_mcp/rails/tool"
65
90
 
66
- Connect via Server-Sent Events at `http://localhost:8080/sse`
91
+ class FindUser < VectorMCP::Rails::Tool
92
+ description "Find a user by id"
93
+ param :id, type: :integer, required: true
94
+
95
+ def call(args, _session)
96
+ user = find!(User, args[:id])
97
+ { id: user.id, email: user.email }
98
+ end
99
+ end
100
+ ```
67
101
 
68
- ## Core Features
102
+ See [docs/rails-setup-guide.md](./docs/rails-setup-guide.md) for a full setup guide.
69
103
 
70
- ### Tools (Functions)
104
+ ## Tools, Resources, and Prompts
71
105
 
72
- Expose functions that LLMs can call:
106
+ Expose callable tools:
73
107
 
74
108
  ```ruby
75
109
  server.register_tool(
76
- name: 'calculate',
77
- description: 'Performs basic math',
110
+ name: "calculate",
111
+ description: "Performs basic math",
78
112
  input_schema: {
79
- type: 'object',
113
+ type: "object",
80
114
  properties: {
81
- operation: { type: 'string', enum: ['add', 'subtract', 'multiply'] },
82
- a: { type: 'number' },
83
- b: { type: 'number' }
115
+ operation: { type: "string", enum: ["add", "subtract", "multiply"] },
116
+ a: { type: "number" },
117
+ b: { type: "number" }
84
118
  },
85
- required: ['operation', 'a', 'b']
119
+ required: ["operation", "a", "b"]
86
120
  }
87
121
  ) do |args|
88
- case args['operation']
89
- when 'add' then args['a'] + args['b']
90
- when 'subtract' then args['a'] - args['b']
91
- when 'multiply' then args['a'] * args['b']
122
+ case args["operation"]
123
+ when "add" then args["a"] + args["b"]
124
+ when "subtract" then args["a"] - args["b"]
125
+ when "multiply" then args["a"] * args["b"]
92
126
  end
93
127
  end
94
128
  ```
95
129
 
96
- ### Resources (Data Sources)
97
-
98
- Provide data that LLMs can read:
130
+ Expose readable resources:
99
131
 
100
132
  ```ruby
101
133
  server.register_resource(
102
- uri: 'file://config.json',
103
- name: 'App Configuration',
104
- description: 'Current application settings'
105
- ) { File.read('config.json') }
134
+ uri: "file://config.json",
135
+ name: "App Configuration",
136
+ description: "Current application settings"
137
+ ) { File.read("config.json") }
106
138
  ```
107
139
 
108
- ### Prompts (Templates)
109
-
110
- Create reusable prompt templates:
140
+ Define prompt templates:
111
141
 
112
142
  ```ruby
113
143
  server.register_prompt(
114
- name: 'code_review',
115
- description: 'Reviews code for best practices',
144
+ name: "code_review",
145
+ description: "Reviews code for best practices",
116
146
  arguments: [
117
- { name: 'language', description: 'Programming language', required: true },
118
- { name: 'code', description: 'Code to review', required: true }
147
+ { name: "language", description: "Programming language", required: true },
148
+ { name: "code", description: "Code to review", required: true }
119
149
  ]
120
150
  ) do |args|
121
151
  {
122
152
  messages: [{
123
- role: 'user',
153
+ role: "user",
124
154
  content: {
125
- type: 'text',
126
- text: "Review this #{args['language']} code:\n\n#{args['code']}"
155
+ type: "text",
156
+ text: "Review this #{args["language"]} code:\n\n#{args["code"]}"
127
157
  }
128
158
  }]
129
159
  }
130
160
  end
131
161
  ```
132
162
 
133
- ## Security Features
163
+ `VectorMCP::Tool` also supports `type: :date` and `type: :datetime`, which are validated as strings in JSON Schema and coerced to `Date` and `Time` before `#call` runs.
134
164
 
135
- VectorMCP provides comprehensive, **opt-in security** for production applications:
165
+ ## Security and Middleware
136
166
 
137
- ### Built-in Input Validation
138
-
139
- All inputs are automatically validated against your schemas:
167
+ VectorMCP keeps security opt-in, but the primitives are built in:
140
168
 
141
169
  ```ruby
142
- # This tool is protected against invalid inputs
143
- server.register_tool(
144
- name: 'process_user',
145
- input_schema: {
146
- type: 'object',
147
- properties: {
148
- email: { type: 'string', format: 'email' },
149
- age: { type: 'integer', minimum: 0, maximum: 150 }
150
- },
151
- required: ['email']
152
- }
153
- ) { |args| "Processing #{args['email']}" }
154
-
155
- # Invalid inputs are automatically rejected:
156
- # ❌ { email: "not-an-email" } -> Validation error
157
- # ❌ { age: -5 } -> Missing required field
158
- # ✅ { email: "user@example.com" } -> Passes validation
159
- ```
160
-
161
- ### Authentication & Authorization
162
-
163
- Secure your MCP server with flexible authentication strategies:
164
-
165
- ```ruby
166
- # API Key Authentication
167
170
  server.enable_authentication!(
168
171
  strategy: :api_key,
169
- keys: ["your-secret-key", "another-key"]
170
- )
171
-
172
- # JWT Token Authentication
173
- server.enable_authentication!(
174
- strategy: :jwt,
175
- secret: ENV["JWT_SECRET"]
172
+ keys: ["your-secret-key"]
176
173
  )
177
174
 
178
- # Custom Authentication Logic
179
- server.enable_authentication!(strategy: :custom) do |request|
180
- api_key = request[:headers]["X-API-Key"]
181
- User.find_by(api_key: api_key) ? { user_id: user.id } : false
182
- end
183
- ```
184
-
185
- ### Fine-Grained Authorization
186
-
187
- Control access to tools, resources, and prompts:
188
-
189
- ```ruby
190
175
  server.enable_authorization! do
191
- # Tool-level access control
192
- authorize_tools do |user, action, tool|
193
- case user[:role]
194
- when "admin" then true
195
- when "user" then !tool.name.start_with?("admin_")
196
- else false
197
- end
198
- end
199
-
200
- # Resource-level permissions
201
- authorize_resources do |user, action, resource|
202
- user[:tenant_id] == resource.tenant_id
176
+ authorize_tools do |user, _action, tool|
177
+ user[:role] == "admin" || !tool.name.start_with?("admin_")
203
178
  end
204
179
  end
205
180
  ```
206
181
 
207
- ### Transport Security
208
-
209
- Security works seamlessly across all transport layers:
210
-
211
- - **Stdio**: Header simulation for desktop applications
212
- - **SSE**: Full HTTP header and query parameter support
213
- - **Request Pipeline**: Automatic authentication and authorization checking
214
-
215
- **👉 [Complete Security Guide →](./security/README.md)**
216
-
217
- Our comprehensive security documentation covers authentication strategies, authorization policies, session management, and real-world examples.
218
-
219
- ## Real-World Examples
220
-
221
- ### File System Server
222
-
223
- ```ruby
224
- server.register_tool(
225
- name: 'read_file',
226
- description: 'Reads a text file',
227
- input_schema: {
228
- type: 'object',
229
- properties: { path: { type: 'string' } },
230
- required: ['path']
231
- }
232
- ) { |args| File.read(args['path']) }
233
- ```
234
-
235
- ### Database Query Tool
182
+ Custom authentication works too:
236
183
 
237
184
  ```ruby
238
- server.register_tool(
239
- name: 'search_users',
240
- description: 'Searches users by name',
241
- input_schema: {
242
- type: 'object',
243
- properties: {
244
- query: { type: 'string', minLength: 1 },
245
- limit: { type: 'integer', minimum: 1, maximum: 100 }
246
- },
247
- required: ['query']
248
- }
249
- ) do |args|
250
- User.where('name ILIKE ?', "%#{args['query']}%")
251
- .limit(args['limit'] || 10)
252
- .to_json
253
- end
254
- ```
255
-
256
- ### API Integration
257
-
258
- ```ruby
259
- server.register_tool(
260
- name: 'get_weather',
261
- description: 'Gets current weather for a city',
262
- input_schema: {
263
- type: 'object',
264
- properties: { city: { type: 'string' } },
265
- required: ['city']
266
- }
267
- ) do |args|
268
- response = HTTP.get("https://api.weather.com/current", params: { city: args['city'] })
269
- response.parse
270
- end
271
- ```
272
-
273
- ---
274
-
275
- ## Advanced Usage
276
-
277
- <details>
278
- <summary><strong>Filesystem Roots & Security</strong></summary>
279
-
280
- Define secure filesystem boundaries:
281
-
282
- ```ruby
283
- # Register allowed directories
284
- server.register_root_from_path('./src', name: 'Source Code')
285
- server.register_root_from_path('./docs', name: 'Documentation')
286
-
287
- # Tools can safely operate within these bounds
288
- server.register_tool(
289
- name: 'list_files',
290
- input_schema: {
291
- type: 'object',
292
- properties: { root_uri: { type: 'string' } },
293
- required: ['root_uri']
294
- }
295
- ) do |args|
296
- root = server.roots[args['root_uri']]
297
- raise 'Invalid root' unless root
298
- Dir.entries(root.path).reject { |f| f.start_with?('.') }
299
- end
300
- ```
301
-
302
- </details>
303
-
304
- <details>
305
- <summary><strong>LLM Sampling (Server → Client)</strong></summary>
306
-
307
- Make requests to the connected LLM:
308
-
309
- ```ruby
310
- server.register_tool(
311
- name: 'generate_summary',
312
- input_schema: {
313
- type: 'object',
314
- properties: { text: { type: 'string' } },
315
- required: ['text']
316
- }
317
- ) do |args, session|
318
- result = session.sample(
319
- messages: [{
320
- role: 'user',
321
- content: { type: 'text', text: "Summarize: #{args['text']}" }
322
- }],
323
- max_tokens: 100
324
- )
325
- result.text_content
185
+ server.enable_authentication!(strategy: :custom) do |request|
186
+ api_key = request[:headers]["X-API-Key"]
187
+ user = User.find_by(api_key: api_key)
188
+ user ? { user_id: user.id, role: user.role } : false
326
189
  end
327
190
  ```
328
191
 
329
- </details>
192
+ For MCP clients that speak OAuth 2.1 (e.g. Claude Desktop), pass a `resource_metadata_url:` to turn on RFC 9728 discovery. Unauthenticated requests to `/mcp` return `401` with a `WWW-Authenticate` header pointing at the configured metadata document, and the client drives the rest of the OAuth dance automatically. See [docs/oauth_resource_server.md](./docs/oauth_resource_server.md) for the feature reference and [docs/rails_oauth_integration.md](./docs/rails_oauth_integration.md) for a full Rails + Doorkeeper recipe.
330
193
 
331
- <details>
332
- <summary><strong>Custom Error Handling</strong></summary>
194
+ Middleware can hook into tool, resource, prompt, sampling, auth, and transport events, including `before_auth`, `after_auth`, `on_auth_error`, `before_request`, `after_response`, and `on_transport_error`.
333
195
 
334
- Use proper MCP error types:
196
+ See [security/README.md](./security/README.md) for the full security guide.
335
197
 
336
- ```ruby
337
- server.register_tool(name: 'risky_operation') do |args|
338
- if args['dangerous']
339
- raise VectorMCP::InvalidParamsError.new('Dangerous operation not allowed')
340
- end
341
-
342
- begin
343
- perform_operation(args)
344
- rescue SomeError => e
345
- raise VectorMCP::InternalError.new('Operation failed')
346
- end
347
- end
348
- ```
198
+ ### Field Anonymization
349
199
 
350
- </details>
351
-
352
- <details>
353
- <summary><strong>Session Information</strong></summary>
354
-
355
- Access client context:
200
+ Keep sensitive string values out of the LLM context by substituting them with stable opaque tokens. Values are tokenized on outbound tool results and restored on inbound tool arguments, so the LLM sees only tokens while your handlers receive the original data.
356
201
 
357
202
  ```ruby
358
- server.register_tool(name: 'client_info') do |args, session|
359
- {
360
- client: session.client_info&.dig('name'),
361
- capabilities: session.client_capabilities,
362
- initialized: session.initialized?
363
- }
364
- end
203
+ anonymizer = VectorMCP::Middleware::Anonymizer.new(
204
+ store: VectorMCP::TokenStore.new,
205
+ field_rules: [
206
+ { pattern: /email/i, prefix: "EMAIL" },
207
+ { pattern: /\bssn\b/i, prefix: "SSN" }
208
+ ]
209
+ )
210
+ anonymizer.install_on(server)
365
211
  ```
366
212
 
367
- </details>
368
-
369
- ---
213
+ ## Transport Notes
370
214
 
371
- ## Integration Examples
215
+ - VectorMCP ships with streamable HTTP as its built-in transport
216
+ - `POST /mcp` accepts a single JSON-RPC request, notification, or response; batch arrays are rejected
217
+ - `GET /mcp` opens an SSE stream for server-initiated messages
218
+ - `DELETE /mcp` terminates the session
219
+ - The server advertises MCP protocol `2025-11-25` and accepts `2025-03-26` and `2024-11-05` headers for compatibility
220
+ - Default allowed origins are restricted to localhost and loopback addresses
372
221
 
373
- ### Claude Desktop
222
+ Initialize a session with curl:
374
223
 
375
- Add to your Claude Desktop configuration:
376
-
377
- ```json
378
- {
379
- "mcpServers": {
380
- "my-ruby-server": {
381
- "command": "ruby",
382
- "args": ["path/to/my_server.rb"]
383
- }
384
- }
385
- }
386
- ```
387
-
388
- ### Web Applications
389
-
390
- ```javascript
391
- // Connect to SSE endpoint
392
- const eventSource = new EventSource('http://localhost:8080/sse');
393
-
394
- eventSource.addEventListener('endpoint', (event) => {
395
- const { uri } = JSON.parse(event.data);
396
-
397
- // Send MCP requests
398
- fetch(uri, {
399
- method: 'POST',
400
- headers: { 'Content-Type': 'application/json' },
401
- body: JSON.stringify({
402
- jsonrpc: '2.0',
403
- id: 1,
404
- method: 'tools/call',
405
- params: { name: 'greet', arguments: { name: 'World' } }
406
- })
407
- });
408
- });
224
+ ```bash
225
+ curl -X POST http://localhost:8080/mcp \
226
+ -H "Content-Type: application/json" \
227
+ -H "Accept: application/json, text/event-stream" \
228
+ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
409
229
  ```
410
230
 
411
- ## Why Choose VectorMCP?
412
-
413
- **🏆 Battle-Tested**: Used in production applications serving thousands of requests
414
-
415
- **⚡ Performance**: Optimized for low latency and high throughput
416
-
417
- **🛡️ Secure by Default**: Comprehensive input validation prevents common attacks
231
+ ## More Features
418
232
 
419
- **📖 Well-Documented**: Extensive examples and clear API documentation
233
+ - Roots via `register_root` and `register_root_from_path`
234
+ - Image resources and image-aware tools/prompts
235
+ - Structured logging with component loggers
236
+ - Server-initiated sampling with streaming/tool-call support
237
+ - Middleware-driven request shaping and observability
420
238
 
421
- **🔧 Extensible**: Easy to customize and extend for your specific needs
239
+ ## Documentation
422
240
 
423
- **🤝 Community**: Active development and responsive maintainer
424
-
425
- ## Examples & Resources
426
-
427
- - **[Examples Directory](./examples/)** - Complete working examples
428
- - **[File System MCP](https://github.com/sergiobayona/file_system_mcp)** - Real-world implementation
429
- - **[MCP Specification](https://modelcontext.dev/)** - Official protocol documentation
430
-
431
- ## Installation & Setup
432
-
433
- ```bash
434
- gem install vector_mcp
435
-
436
- # Or in your Gemfile
437
- gem 'vector_mcp'
438
- ```
241
+ - [CHANGELOG.md](./CHANGELOG.md)
242
+ - [examples/](./examples/)
243
+ - [docs/rails-setup-guide.md](./docs/rails-setup-guide.md)
244
+ - [docs/rails_oauth_integration.md](./docs/rails_oauth_integration.md)
245
+ - [docs/oauth_resource_server.md](./docs/oauth_resource_server.md)
246
+ - [docs/streamable-http-spec-compliance.md](./docs/streamable-http-spec-compliance.md)
247
+ - [security/README.md](./security/README.md)
248
+ - [MCP Specification](https://modelcontextprotocol.io/)
439
249
 
440
250
  ## Contributing
441
251
 
442
- Bug reports and pull requests welcome on [GitHub](https://github.com/sergiobayona/vector_mcp).
252
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/sergiobayona/vector_mcp).
443
253
 
444
254
  ## License
445
255
 
446
- Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
256
+ Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
@@ -29,6 +29,36 @@ module VectorMCP
29
29
  }.compact # Remove nil values
30
30
  end
31
31
 
32
+ # Class method to create a tool whose input_schema already declares a
33
+ # base64-encoded image property. Mirrors Resource.from_image_file and
34
+ # Prompt.with_image_support — keeps schema-building logic with the
35
+ # definition, not with the registry.
36
+ #
37
+ # @param name [String] Unique name for the tool.
38
+ # @param description [String] Human-readable description.
39
+ # @param image_parameter [String] Name of the image parameter.
40
+ # @param additional_parameters [Hash] Additional JSON Schema properties.
41
+ # @param required_parameters [Array<String>] Required parameter names.
42
+ # @param handler [Proc] Tool handler block.
43
+ # @return [Tool]
44
+ def self.with_image_support(name:, description:, image_parameter: "image",
45
+ additional_parameters: {}, required_parameters: [], &handler)
46
+ image_property = {
47
+ type: "string",
48
+ description: "Base64 encoded image data or file path to image",
49
+ contentEncoding: "base64",
50
+ contentMediaType: "image/*"
51
+ }
52
+
53
+ input_schema = {
54
+ type: "object",
55
+ properties: { image_parameter => image_property }.merge(additional_parameters),
56
+ required: required_parameters
57
+ }
58
+
59
+ new(name, description, input_schema, handler)
60
+ end
61
+
32
62
  # Checks if this tool supports image inputs based on its input schema.
33
63
  # @return [Boolean] True if the tool's input schema includes image properties.
34
64
  def supports_image_input?