explicit 0.2.15 → 0.2.17
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.
- checksums.yaml +4 -4
- data/README.md +158 -9
- data/app/views/explicit/documentation/type/_big_decimal.html.erb +16 -0
- data/config/locales/en.yml +1 -0
- data/lib/explicit/configuration.rb +9 -0
- data/lib/explicit/documentation/builder.rb +1 -1
- data/lib/explicit/documentation/output/swagger.rb +2 -0
- data/lib/explicit/mcp_server/builder.rb +50 -0
- data/lib/explicit/mcp_server/request.rb +42 -0
- data/lib/explicit/mcp_server/response.rb +29 -0
- data/lib/explicit/mcp_server/router.rb +84 -0
- data/lib/explicit/mcp_server/tool.rb +20 -0
- data/lib/explicit/mcp_server.rb +39 -0
- data/lib/explicit/request.rb +35 -1
- data/lib/explicit/type/agreement.rb +7 -9
- data/lib/explicit/type/any.rb +4 -4
- data/lib/explicit/type/array.rb +6 -8
- data/lib/explicit/type/big_decimal.rb +35 -14
- data/lib/explicit/type/boolean.rb +4 -6
- data/lib/explicit/type/date.rb +9 -11
- data/lib/explicit/type/date_range.rb +11 -13
- data/lib/explicit/type/date_time_iso8601.rb +9 -11
- data/lib/explicit/type/date_time_iso8601_range.rb +10 -12
- data/lib/explicit/type/date_time_unix_epoch.rb +9 -11
- data/lib/explicit/type/enum.rb +5 -7
- data/lib/explicit/type/file.rb +9 -11
- data/lib/explicit/type/float.rb +12 -14
- data/lib/explicit/type/hash.rb +8 -10
- data/lib/explicit/type/integer.rb +12 -14
- data/lib/explicit/type/literal.rb +5 -7
- data/lib/explicit/type/one_of.rb +3 -5
- data/lib/explicit/type/record.rb +12 -25
- data/lib/explicit/type/string.rb +11 -13
- data/lib/explicit/type.rb +10 -2
- data/lib/explicit/version.rb +1 -1
- data/lib/explicit.rb +7 -0
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 28e91e624223bafc22d197130a6ae5973f6a2364f6dd192d8e2f70b74fe88968
|
4
|
+
data.tar.gz: 3fcacbe34bd45dc3b988f703f05928a65af46f284b498a64e83f12539cc2ef86
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bc53aeced2b49c6667fb723a681d8e01b8e2529713a07f142281d2b2bcd40dc619c435c08ad7d7aee38aeceac7e053fd2d46a8e2d438bf82211a4fbd63ba12b7
|
7
|
+
data.tar.gz: cf4995522dab379e5277ac328e55968d0bc30b8850d64fcf7fb25b7afb9eae0c0c865df0813b7578dbff0d91f179b10a32e51b4c9b797932ab1aec95091c54ba
|
data/README.md
CHANGED
@@ -14,7 +14,10 @@ documented types at runtime.
|
|
14
14
|
5. [Writing tests](#writing-tests)
|
15
15
|
6. [Publishing documentation](#publishing-documentation)
|
16
16
|
- [Adding request examples](#adding-request-examples)
|
17
|
-
7.
|
17
|
+
7. [MCP](#mcp)
|
18
|
+
- [Tool configuration](#tool-configuration)
|
19
|
+
- [Security](#security-and-authorization)
|
20
|
+
8. Types
|
18
21
|
- [Agreement](#agreement)
|
19
22
|
- [Any](#any)
|
20
23
|
- [Array](#array)
|
@@ -37,11 +40,12 @@ documented types at runtime.
|
|
37
40
|
- [One of](#one-of)
|
38
41
|
- [Record](#record)
|
39
42
|
- [String](#string)
|
40
|
-
|
43
|
+
9. [Configuration](#configuration)
|
41
44
|
- [Changing examples file path](#changing-examples-file-path)
|
42
45
|
- [Customizing error messages](#customizing-error-messages)
|
43
46
|
- [Customizing error serialization](#customizing-error-serialization)
|
44
47
|
- [Raise on invalid example](#raise-on-invalid-example)
|
48
|
+
- [CORS](#cors)
|
45
49
|
|
46
50
|
# Installation
|
47
51
|
|
@@ -75,7 +79,7 @@ available:
|
|
75
79
|
- `description: "text"` - Adds a documentation to the param. Markdown supported.
|
76
80
|
- `response(status, type)` - Adds a response type. You can add multiple
|
77
81
|
responses with different formats.
|
78
|
-
- `
|
82
|
+
- `example(params:, headers:, response:)` - Adds an example to the
|
79
83
|
documentation. [See more details here](#adding-request-examples).
|
80
84
|
- `base_url(url)` - Sets the host for this API. For example: "https://api.myapp.com".
|
81
85
|
Meant to be used with [request composition](#reusing-requests).
|
@@ -314,12 +318,12 @@ end
|
|
314
318
|
|
315
319
|
You can add request examples in two different ways:
|
316
320
|
|
317
|
-
1. Manually add an example with `
|
321
|
+
1. Manually add an example with `example(params:, headers:, response:)`
|
318
322
|
2. Automatically save examples from tests
|
319
323
|
|
320
324
|
### 1. Manually adding examples
|
321
325
|
|
322
|
-
In a request, call `
|
326
|
+
In a request, call `example(params:, headers:, response:)` after declaring
|
323
327
|
params and responses. It's important the example comes after params and
|
324
328
|
responses to make sure it actually follows the type definition.
|
325
329
|
|
@@ -329,7 +333,7 @@ For example:
|
|
329
333
|
Request = Explicit::Request.new do
|
330
334
|
# ... other configs, params and responses
|
331
335
|
|
332
|
-
|
336
|
+
example(
|
333
337
|
params: {
|
334
338
|
name: "Bilbo baggins",
|
335
339
|
email: "bilbo@shire.com",
|
@@ -356,8 +360,8 @@ way you like. For example:
|
|
356
360
|
Request = Explicit::Request.new do
|
357
361
|
# ... other configs, params and responses
|
358
362
|
|
359
|
-
|
360
|
-
|
363
|
+
example MyApp::Examples::REQUEST_1
|
364
|
+
example MyApp::Examples::REQUEST_2
|
361
365
|
end
|
362
366
|
```
|
363
367
|
|
@@ -397,6 +401,137 @@ Whenever you wish to refresh the examples file run the test suite with the ENV
|
|
397
401
|
**Important: be careful not to leak any sensitive data when persisting
|
398
402
|
examples from tests**
|
399
403
|
|
404
|
+
# MCP
|
405
|
+
|
406
|
+
You can expose your API endpoints as tools for chat clients by mounting an MCP
|
407
|
+
server. The MCP server acts as a proxy receiving tool calls and forwarding them
|
408
|
+
to your existing REST API controllers. Your controllers remain the source of
|
409
|
+
truth and the MCP server simply provides a tool-compatible interface.
|
410
|
+
|
411
|
+
To build an MCP server, instantiate `::Explicit::MCPServer` and add the requests
|
412
|
+
you wish to expose. The following methods are available:
|
413
|
+
|
414
|
+
- `name(str)` - Sets the name of the MCP Server which is displayed in the MCP
|
415
|
+
client.
|
416
|
+
- `version(str)` - Sets the version of the MCP server which is displayed in the
|
417
|
+
MCP client
|
418
|
+
- `add(request)` - Exposes a request as a tool in the MCP server.
|
419
|
+
|
420
|
+
For example:
|
421
|
+
|
422
|
+
```ruby
|
423
|
+
module MyApp::API::V1
|
424
|
+
MCPServer = Explicit::MCPServer.new do
|
425
|
+
name "My app"
|
426
|
+
version "1.0.0"
|
427
|
+
|
428
|
+
add ArticlesController::CreateRequest
|
429
|
+
add ArticlesController::UpdateRequest
|
430
|
+
add ArticlesController::DestroyRequest
|
431
|
+
|
432
|
+
def authorize(**)
|
433
|
+
true
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
```
|
438
|
+
|
439
|
+
Then, mount the MCP Server in your `routes.rb`:
|
440
|
+
|
441
|
+
```ruby
|
442
|
+
Rails.application.routes.draw do
|
443
|
+
mount MyApp::API::V1::MCPServer => "/api/v1/mcp"
|
444
|
+
end
|
445
|
+
```
|
446
|
+
|
447
|
+
### Tool configuration
|
448
|
+
|
449
|
+
The following methods are available in `Explicit::Request` to configure the MCP
|
450
|
+
tool. They're all optional and the MCP server still works correctly using the
|
451
|
+
request's default title, description and params.
|
452
|
+
|
453
|
+
- `mcp_tool_name(name)` - Sets the unique identifier for the tool. Should be a
|
454
|
+
string with only ASCII letters, numbers and underscore. By default it is set
|
455
|
+
to a normalized version of the route's path.
|
456
|
+
- `mcp_tool_description(description)` - Sets the description of the tool.
|
457
|
+
Markdown supported. By default it is set to the request description.
|
458
|
+
- `mcp_tool_title(title)` - Sets the human readable name for the tool. By
|
459
|
+
default it is set to the request's title.
|
460
|
+
- `mcp_tool_read_only_hint(true/false)` - If true, the tool does not modify its
|
461
|
+
environment.
|
462
|
+
- `mcp_tool_destructive_hint(true/false)` - If true, the tool may perform destructive
|
463
|
+
updates.
|
464
|
+
- `mcp_tool_idempotent_hint(true/false)` - If true, repeated calls with same args
|
465
|
+
have no additional effect.
|
466
|
+
- `mcp_tool_open_world_hint(true/false)` - If true, tool interacts with external
|
467
|
+
entities.
|
468
|
+
|
469
|
+
For example:
|
470
|
+
|
471
|
+
```ruby
|
472
|
+
Request = Explicit::Request.new do
|
473
|
+
# ... other request config
|
474
|
+
|
475
|
+
mcp_tool_name "get_article_by_id"
|
476
|
+
mcp_tool_title "Get article by id"
|
477
|
+
mcp_tool_read_only_hint true
|
478
|
+
mcp_tool_destructive_hint false
|
479
|
+
mcp_tool_idempotent_hint true
|
480
|
+
mcp_tool_open_world_hint false
|
481
|
+
|
482
|
+
mcp_tool_description <<~TEXT
|
483
|
+
Finds the article by the specified id and returns the title, body and
|
484
|
+
published_at date.
|
485
|
+
TEXT
|
486
|
+
end
|
487
|
+
```
|
488
|
+
|
489
|
+
### Security
|
490
|
+
|
491
|
+
There are two considerations when securing your MCP server:
|
492
|
+
|
493
|
+
1. **Authorize the MCP tool call**
|
494
|
+
You should authorize the action based on a unique attribute present in the
|
495
|
+
request's params or headers. For example, you should share a URL with your
|
496
|
+
customers similar to this one:
|
497
|
+
`https://myapp.com/api/v1/mcp?key=d17c08d5-968c-497f-8db2-ec958d45b447`.
|
498
|
+
Then, in the `authorize` method, you'd use the `key` to find the
|
499
|
+
user/customer/account.
|
500
|
+
2. **Authenticate the REST API**
|
501
|
+
Your API probably has an authentication mechanism that is different from the
|
502
|
+
MCP server, such as bearer tokens specified in request headers. To
|
503
|
+
authenticate the API you can either 1) use `proxy_with(headers:)` or 2)
|
504
|
+
share the current user using `ActiveSupport::CurrentAttributes`.
|
505
|
+
|
506
|
+
To secure your MCPServer you must implement the `authorize` method in your
|
507
|
+
`Explicit::MCPServer`. This method is invoked on all requests received by the
|
508
|
+
MCP server. The following arguments are given to `authorize`:
|
509
|
+
|
510
|
+
* `params` - hash with request's query string values
|
511
|
+
* `headers` - hash with the request's HTTP headers
|
512
|
+
|
513
|
+
If you return `false` then the request will be rejected immediatly without ever
|
514
|
+
hitting your API controllers. For example:
|
515
|
+
|
516
|
+
```ruby
|
517
|
+
module MyApp::API::V1
|
518
|
+
MCPServer = Explicit::MCPServer.new do
|
519
|
+
# ... other configurations
|
520
|
+
|
521
|
+
def authorize(params:, headers:)
|
522
|
+
user = ::User.find_by(api_key: params[:key])
|
523
|
+
return false if user.blank?
|
524
|
+
|
525
|
+
# 1) proxy the request to controllers with headers
|
526
|
+
proxy_with headers: { "Authorization" => "Bearer #{user.api_key}" }
|
527
|
+
|
528
|
+
# 2) or share the user with controllers using ActiveSupport::CurrentAttributes
|
529
|
+
Current.user = user
|
530
|
+
end
|
531
|
+
end
|
532
|
+
end
|
533
|
+
```
|
534
|
+
|
400
535
|
# Types
|
401
536
|
|
402
537
|
### Agreement
|
@@ -418,7 +553,6 @@ and `1`.
|
|
418
553
|
Allows all values, including null. Useful when documenting a proxy that
|
419
554
|
responds with whatever value the other service returned.
|
420
555
|
|
421
|
-
|
422
556
|
### Array
|
423
557
|
|
424
558
|
```ruby
|
@@ -434,6 +568,8 @@ value is invalid then the array is invalid.
|
|
434
568
|
|
435
569
|
```ruby
|
436
570
|
:big_decimal
|
571
|
+
[:big_decimal, negative: false]
|
572
|
+
[:big_decimal, positive: true]
|
437
573
|
[:big_decimal, min: 0] # inclusive
|
438
574
|
[:big_decimal, max: 100] # inclusive
|
439
575
|
```
|
@@ -706,7 +842,20 @@ end
|
|
706
842
|
|
707
843
|
### Raise on invalid example
|
708
844
|
|
845
|
+
When adding a request example with `example(params:, response:)`, the response
|
846
|
+
must match the documented types. If it doesn't match, Explicit logs a warning,
|
847
|
+
but you can choose a more strict behaviour that raises an error instead.
|
848
|
+
|
709
849
|
```ruby
|
710
850
|
config.raise_on_invalid_example = true # default is false
|
711
851
|
config.raise_on_invalid_example = ::Rails.env.development? # recommended
|
712
852
|
```
|
853
|
+
|
854
|
+
### CORS
|
855
|
+
|
856
|
+
Enable/disable [CORS headers](https://github.com/luizpvas/explicit/blob/main/lib/explicit/documentation/output/swagger.rb#L67-L74)
|
857
|
+
support.
|
858
|
+
|
859
|
+
```ruby
|
860
|
+
config.cors_enabled = true
|
861
|
+
```
|
@@ -5,4 +5,20 @@
|
|
5
5
|
<%= type_constraints do %>
|
6
6
|
<%= type_constraint "min:", type.min if type.min.present? %>
|
7
7
|
<%= type_constraint "max:", type.max if type.max.present? %>
|
8
|
+
|
9
|
+
<% if type.negative == false %>
|
10
|
+
<%= type_constraint "not", "negative" %>
|
11
|
+
<% end %>
|
12
|
+
|
13
|
+
<% if type.negative == true %>
|
14
|
+
<%= type_constraint "only", "negative" %>
|
15
|
+
<% end %>
|
16
|
+
|
17
|
+
<% if type.positive == false %>
|
18
|
+
<%= type_constraint "not", "positive" %>
|
19
|
+
<% end %>
|
20
|
+
|
21
|
+
<% if type.positive == true %>
|
22
|
+
<%= type_constraint "only", "positive" %>
|
23
|
+
<% end %>
|
8
24
|
<% end %>
|
data/config/locales/en.yml
CHANGED
@@ -51,6 +51,7 @@ en:
|
|
51
51
|
format: "must have format %{regex}"
|
52
52
|
swagger:
|
53
53
|
agreement: "* Must be accepted (true)"
|
54
|
+
big_decimal_format: "* String-encoded decimal number. For example: '123.45'"
|
54
55
|
big_decimal_min: "* Minimum: %{min}"
|
55
56
|
big_decimal_max: "* Maximum: %{max}"
|
56
57
|
date_range: '* The value must be a range between two dates in the format of: "YYYY-MM-DD..YYYY-MM-DD"'
|
@@ -6,6 +6,7 @@ module Explicit
|
|
6
6
|
class Configuration
|
7
7
|
def initialize
|
8
8
|
@rescue_from_invalid_params = true
|
9
|
+
@cors_enabled = !defined?(::Rack::Cors)
|
9
10
|
end
|
10
11
|
|
11
12
|
def request_examples_file_path=(path)
|
@@ -36,6 +37,14 @@ module Explicit
|
|
36
37
|
@raise_on_invalid_example
|
37
38
|
end
|
38
39
|
|
40
|
+
def cors_enabled=(enabled)
|
41
|
+
@cors_enabled = enabled
|
42
|
+
end
|
43
|
+
|
44
|
+
def cors_enabled?
|
45
|
+
@cors_enabled
|
46
|
+
end
|
47
|
+
|
39
48
|
def test_runner=(test_runner)
|
40
49
|
@test_runner = test_runner
|
41
50
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Explicit::MCPServer::Builder
|
4
|
+
def initialize
|
5
|
+
@tools = []
|
6
|
+
end
|
7
|
+
|
8
|
+
def name(name) = (@name = name)
|
9
|
+
def get_name = @name
|
10
|
+
|
11
|
+
def version(version) = (@version = version)
|
12
|
+
def get_version = @version
|
13
|
+
|
14
|
+
def add(request)
|
15
|
+
@tools << ::Explicit::MCPServer::Tool.new(request)
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(env)
|
19
|
+
request = ::Explicit::MCPServer::Request.from_rack_env(env)
|
20
|
+
|
21
|
+
if respond_to?(:authorize)
|
22
|
+
params = ::Rack::Utils.parse_nested_query(env["QUERY_STRING"]).with_indifferent_access
|
23
|
+
|
24
|
+
case authorize(params:, headers: request.headers)
|
25
|
+
in { headers: }
|
26
|
+
request.headers.merge!(headers)
|
27
|
+
in false
|
28
|
+
return [403, {}, []]
|
29
|
+
else
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
response = router.handle(request)
|
35
|
+
|
36
|
+
[200, { "Content-Type" => "application/json" }, [response.to_json]]
|
37
|
+
end
|
38
|
+
|
39
|
+
def router
|
40
|
+
@router ||= ::Explicit::MCPServer::Router.new(
|
41
|
+
name: @name,
|
42
|
+
version: @version,
|
43
|
+
tools: @tools
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
def proxy_with(headers:)
|
48
|
+
{ headers: }
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack/utils"
|
4
|
+
|
5
|
+
module Explicit::MCPServer
|
6
|
+
Request = ::Data.define(:id, :method, :params, :host, :headers) do
|
7
|
+
def self.from_rack_env(env)
|
8
|
+
headers = env.each_with_object({}) do |(key, value), hash|
|
9
|
+
if key.start_with?("HTTP_") && key != "HTTP_HOST"
|
10
|
+
header_name = key[5..-1].split("_").map(&:capitalize).join("-")
|
11
|
+
hash[header_name] = value
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
body = ::JSON.parse(env["rack.input"].read)
|
16
|
+
|
17
|
+
new(
|
18
|
+
id: body["id"],
|
19
|
+
method: body["method"],
|
20
|
+
params: body["params"],
|
21
|
+
host: env["HTTP_HOST"],
|
22
|
+
headers:
|
23
|
+
)
|
24
|
+
rescue ::JSON::ParserError
|
25
|
+
new(
|
26
|
+
id: nil,
|
27
|
+
method: nil,
|
28
|
+
params: nil,
|
29
|
+
host: env["HTTP_HOST"],
|
30
|
+
headers: headers
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
def result(value)
|
35
|
+
::Explicit::MCPServer::Response::Result.new(id:, value:)
|
36
|
+
end
|
37
|
+
|
38
|
+
def error(value)
|
39
|
+
::Explicit::MCPServer::Response::Error.new(id:, value:)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Explicit::MCPServer::Response
|
4
|
+
Result = Data.define(:id, :value) do
|
5
|
+
def result? = true
|
6
|
+
def error? = false
|
7
|
+
|
8
|
+
def to_json
|
9
|
+
{
|
10
|
+
jsonrpc: "2.0",
|
11
|
+
id:,
|
12
|
+
result: value
|
13
|
+
}.to_json
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
Error = Data.define(:id, :value) do
|
18
|
+
def result? = false
|
19
|
+
def error? = true
|
20
|
+
|
21
|
+
def to_json
|
22
|
+
{
|
23
|
+
jsonrpc: "2.0",
|
24
|
+
id:,
|
25
|
+
error: value
|
26
|
+
}.to_json
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Explicit::MCPServer::Router
|
4
|
+
def initialize(name:, version:, tools:)
|
5
|
+
@name = name
|
6
|
+
@version = version
|
7
|
+
@tools = tools.index_by { _1.request.get_mcp_tool_name }
|
8
|
+
end
|
9
|
+
|
10
|
+
def handle(request)
|
11
|
+
case request.method
|
12
|
+
when "ping" then noop(request)
|
13
|
+
when "initialize" then initialize_(request)
|
14
|
+
when "notifications/initialized" then noop(request)
|
15
|
+
when "tools/list" then tools_list(request)
|
16
|
+
when "tools/call" then tools_call(request)
|
17
|
+
else noop(request)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def noop(request)
|
24
|
+
request.result({})
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize_(request)
|
28
|
+
request.result({
|
29
|
+
protocolVersion: "2024-11-05",
|
30
|
+
capabilities: {
|
31
|
+
tools: {
|
32
|
+
listChanged: false
|
33
|
+
}
|
34
|
+
},
|
35
|
+
serverInfo: {
|
36
|
+
name: @name,
|
37
|
+
version: @version
|
38
|
+
}
|
39
|
+
})
|
40
|
+
end
|
41
|
+
|
42
|
+
def tools_list(request)
|
43
|
+
request.result({ tools: @tools.values.map(&:serialize) })
|
44
|
+
end
|
45
|
+
|
46
|
+
def tools_call(request)
|
47
|
+
tool_name = request.params["name"]
|
48
|
+
arguments = request.params["arguments"]
|
49
|
+
|
50
|
+
tool = @tools[tool_name]
|
51
|
+
|
52
|
+
if !tool
|
53
|
+
return request.error({ code: -32602, message: "tool not found" })
|
54
|
+
end
|
55
|
+
|
56
|
+
session = ::ActionDispatch::Integration::Session.new(::Rails.application)
|
57
|
+
session.host = request.host
|
58
|
+
route = tool.request.routes.first
|
59
|
+
|
60
|
+
path = [
|
61
|
+
tool.request.get_base_path,
|
62
|
+
route.replace_path_params(arguments)
|
63
|
+
].compact_blank.join
|
64
|
+
|
65
|
+
path, params =
|
66
|
+
if route.accepts_request_body?
|
67
|
+
[path, arguments]
|
68
|
+
else
|
69
|
+
["#{path}?#{arguments.to_query}", nil]
|
70
|
+
end
|
71
|
+
|
72
|
+
session.process(route.method, path, params:, headers: request.headers)
|
73
|
+
|
74
|
+
request.result({
|
75
|
+
content: [
|
76
|
+
{
|
77
|
+
type: "text",
|
78
|
+
text: session.response.body
|
79
|
+
}
|
80
|
+
],
|
81
|
+
isError: session.response.status < 200 || session.response.status > 299
|
82
|
+
})
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Explicit::MCPServer
|
4
|
+
Tool = ::Data.define(:request) do
|
5
|
+
def serialize
|
6
|
+
{
|
7
|
+
name: request.get_mcp_tool_name,
|
8
|
+
description: request.get_mcp_tool_description,
|
9
|
+
inputSchema: request.params_type.mcp_schema,
|
10
|
+
annotations: {
|
11
|
+
title: request.get_mcp_tool_title,
|
12
|
+
readOnlyHint: request.get_mcp_tool_read_only_hint,
|
13
|
+
destructiveHint: request.get_mcp_tool_destructive_hint,
|
14
|
+
idempotentHint: request.get_mcp_tool_idempotent_hint,
|
15
|
+
openWorldHint: request.get_mcp_tool_open_world_hint
|
16
|
+
}.compact
|
17
|
+
}.compact
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Explicit::MCPServer
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def new(&block)
|
7
|
+
engine = ::Class.new(::Rails::Engine)
|
8
|
+
|
9
|
+
builder = Builder.new.tap do |builder|
|
10
|
+
builder.instance_eval(&block)
|
11
|
+
end
|
12
|
+
|
13
|
+
if builder.get_name.blank?
|
14
|
+
raise <<~TEXT
|
15
|
+
MCP servers must have a name. For example:
|
16
|
+
|
17
|
+
Explicit::MCPServer.new do
|
18
|
+
name "My app"
|
19
|
+
end
|
20
|
+
TEXT
|
21
|
+
end
|
22
|
+
|
23
|
+
if builder.get_version.blank?
|
24
|
+
raise <<~TEXT
|
25
|
+
MCP servers must have a version. For example:
|
26
|
+
|
27
|
+
Explicit::MCPServer.new do
|
28
|
+
version "1.0.0"
|
29
|
+
end
|
30
|
+
TEXT
|
31
|
+
end
|
32
|
+
|
33
|
+
engine.routes.draw do
|
34
|
+
match "/", to: builder, as: :explicit_mcp, via: :all
|
35
|
+
end
|
36
|
+
|
37
|
+
engine
|
38
|
+
end
|
39
|
+
end
|
data/lib/explicit/request.rb
CHANGED
@@ -68,6 +68,39 @@ class Explicit::Request
|
|
68
68
|
def description(markdown) = (@description = markdown)
|
69
69
|
def get_description = @description
|
70
70
|
|
71
|
+
concerning :MCP do
|
72
|
+
def mcp_tool_name(name) = (@mcp_tool_name = name)
|
73
|
+
def get_mcp_tool_name
|
74
|
+
return @mcp_tool_name if @mcp_tool_name.present?
|
75
|
+
|
76
|
+
normalized = @routes.first.method.to_s + @routes.first.path.gsub(/\//, "_")
|
77
|
+
|
78
|
+
@routes.first.params.each do |param_name|
|
79
|
+
normalized = normalized.gsub(":#{param_name}", "by_#{param_name}")
|
80
|
+
end
|
81
|
+
|
82
|
+
normalized
|
83
|
+
end
|
84
|
+
|
85
|
+
def mcp_tool_description(markdown) = (@mcp_tool_description = markdown)
|
86
|
+
def get_mcp_tool_description = @mcp_tool_description || get_description
|
87
|
+
|
88
|
+
def mcp_tool_title(title) = (@mcp_tool_title = title)
|
89
|
+
def get_mcp_tool_title = @mcp_tool_title || get_title
|
90
|
+
|
91
|
+
def mcp_tool_read_only_hint(bool) = (@mcp_tool_read_only_hint = bool)
|
92
|
+
def get_mcp_tool_read_only_hint = @mcp_tool_read_only_hint
|
93
|
+
|
94
|
+
def mcp_tool_destructive_hint(bool) = (@mcp_tool_destructive_hint = bool)
|
95
|
+
def get_mcp_tool_destructive_hint = @mcp_tool_destructive_hint
|
96
|
+
|
97
|
+
def mcp_tool_idempotent_hint(bool) = (@mcp_tool_idempotent_hint = bool)
|
98
|
+
def get_mcp_tool_idempotent_hint = @mcp_tool_idempotent_hint
|
99
|
+
|
100
|
+
def mcp_tool_open_world_hint(bool) = (@mcp_tool_open_world_hint = bool)
|
101
|
+
def get_mcp_tool_open_world_hint = @mcp_tool_open_world_hint
|
102
|
+
end
|
103
|
+
|
71
104
|
def header(name, type, **options)
|
72
105
|
raise ArgumentError("duplicated header #{name}") if @headers.key?(name)
|
73
106
|
|
@@ -108,7 +141,7 @@ class Explicit::Request
|
|
108
141
|
@responses[status] << typespec
|
109
142
|
end
|
110
143
|
|
111
|
-
def
|
144
|
+
def example(params:, response:, headers: {})
|
112
145
|
raise ArgumentError.new("missing :status in response") if !response.key?(:status)
|
113
146
|
raise ArgumentError.new("missing :data in response") if !response.key?(:data)
|
114
147
|
|
@@ -130,6 +163,7 @@ class Explicit::Request
|
|
130
163
|
|
131
164
|
@examples[response.status] << Example.new(request: self, params:, headers:, response:)
|
132
165
|
end
|
166
|
+
alias add_example example
|
133
167
|
|
134
168
|
def validate!(values)
|
135
169
|
case params_type.validate(values)
|
@@ -26,14 +26,12 @@ class Explicit::Type::Agreement < Explicit::Type
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
})
|
37
|
-
end
|
29
|
+
def json_schema(flavour)
|
30
|
+
{
|
31
|
+
type: "boolean",
|
32
|
+
description_topics: [
|
33
|
+
swagger_i18n("agreement")
|
34
|
+
]
|
35
|
+
}
|
38
36
|
end
|
39
37
|
end
|
data/lib/explicit/type/any.rb
CHANGED
data/lib/explicit/type/array.rb
CHANGED
@@ -43,13 +43,11 @@ class Explicit::Type::Array < Explicit::Type
|
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
})
|
53
|
-
end
|
46
|
+
def json_schema(flavour)
|
47
|
+
{
|
48
|
+
type: "array",
|
49
|
+
items: item_type.mcp_schema,
|
50
|
+
minItems: empty ? 0 : 1
|
51
|
+
}
|
54
52
|
end
|
55
53
|
end
|
@@ -1,11 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class Explicit::Type::BigDecimal < Explicit::Type
|
4
|
-
attr_reader :min, :max
|
4
|
+
attr_reader :min, :max, :negative, :positive
|
5
5
|
|
6
|
-
def initialize(min: nil, max: nil)
|
6
|
+
def initialize(min: nil, max: nil, negative: nil, positive: nil)
|
7
7
|
@min = min
|
8
8
|
@max = max
|
9
|
+
@negative = negative
|
10
|
+
@positive = positive
|
9
11
|
end
|
10
12
|
|
11
13
|
def validate(value)
|
@@ -23,6 +25,22 @@ class Explicit::Type::BigDecimal < Explicit::Type
|
|
23
25
|
return error_i18n("max", max:)
|
24
26
|
end
|
25
27
|
|
28
|
+
if negative == false && decimal_value < 0
|
29
|
+
return error_i18n("not_negative")
|
30
|
+
end
|
31
|
+
|
32
|
+
if negative == true && decimal_value >= 0
|
33
|
+
return error_i18n("only_negative")
|
34
|
+
end
|
35
|
+
|
36
|
+
if positive == false && decimal_value > 0
|
37
|
+
return error_i18n("not_positive")
|
38
|
+
end
|
39
|
+
|
40
|
+
if positive == true && decimal_value <= 0
|
41
|
+
return error_i18n("only_positive")
|
42
|
+
end
|
43
|
+
|
26
44
|
[:ok, decimal_value]
|
27
45
|
rescue ArgumentError
|
28
46
|
return error_i18n("big_decimal")
|
@@ -42,17 +60,20 @@ class Explicit::Type::BigDecimal < Explicit::Type
|
|
42
60
|
end
|
43
61
|
end
|
44
62
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
63
|
+
def json_schema(flavour)
|
64
|
+
{
|
65
|
+
type: "string",
|
66
|
+
pattern: /^\d*\.?\d*$/.inspect[1..-2],
|
67
|
+
format: "decimal number",
|
68
|
+
description_topics: [
|
69
|
+
swagger_i18n("big_decimal_format"),
|
70
|
+
min&.then { swagger_i18n("big_decimal_min", min: _1) },
|
71
|
+
max&.then { swagger_i18n("big_decimal_max", max: _1) },
|
72
|
+
positive == false ? swagger_i18n("number_not_positive") : nil,
|
73
|
+
positive == true ? swagger_i18n("number_only_positive") : nil,
|
74
|
+
negative == false ? swagger_i18n("number_not_negative") : nil,
|
75
|
+
negative == true ? swagger_i18n("number_only_negative") : nil
|
76
|
+
].compact
|
77
|
+
}
|
57
78
|
end
|
58
79
|
end
|
data/lib/explicit/type/date.rb
CHANGED
@@ -51,16 +51,14 @@ class Explicit::Type::Date < Explicit::Type
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
})
|
64
|
-
end
|
54
|
+
def json_schema(flavour)
|
55
|
+
{
|
56
|
+
type: "string",
|
57
|
+
pattern: /\d{4}-\d{2}-\d{2}/.inspect[1..-2],
|
58
|
+
format: "date",
|
59
|
+
description_topics: [
|
60
|
+
swagger_i18n("date_format")
|
61
|
+
]
|
62
|
+
}
|
65
63
|
end
|
66
64
|
end
|
@@ -78,18 +78,16 @@ class Explicit::Type::DateRange < Explicit::Type
|
|
78
78
|
end
|
79
79
|
end
|
80
80
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
})
|
93
|
-
end
|
81
|
+
def json_schema(flavour)
|
82
|
+
{
|
83
|
+
type: "string",
|
84
|
+
pattern: FORMAT.inspect[1..-2],
|
85
|
+
format: "date range",
|
86
|
+
description_topics: [
|
87
|
+
swagger_i18n("date_range"),
|
88
|
+
min_range&.then { swagger_i18n("date_range_min_range", min_range: _1.inspect) },
|
89
|
+
max_range&.then { swagger_i18n("date_range_max_range", max_range: _1.inspect) },
|
90
|
+
]
|
91
|
+
}
|
94
92
|
end
|
95
93
|
end
|
@@ -52,15 +52,13 @@ class Explicit::Type::DateTimeISO8601 < Explicit::Type
|
|
52
52
|
end
|
53
53
|
end
|
54
54
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
})
|
64
|
-
end
|
55
|
+
def json_schema(flavour)
|
56
|
+
{
|
57
|
+
type: "string",
|
58
|
+
format: "date-time",
|
59
|
+
description_topics: [
|
60
|
+
swagger_i18n("date_time_iso8601")
|
61
|
+
]
|
62
|
+
}
|
65
63
|
end
|
66
|
-
end
|
64
|
+
end
|
@@ -92,17 +92,15 @@ class Explicit::Type::DateTimeISO8601Range < Explicit::Type
|
|
92
92
|
end
|
93
93
|
end
|
94
94
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
})
|
106
|
-
end
|
95
|
+
def json_schema(flavour)
|
96
|
+
{
|
97
|
+
type: "string",
|
98
|
+
format: "date time range",
|
99
|
+
description_topics: [
|
100
|
+
swagger_i18n("date_time_iso8601_range"),
|
101
|
+
min_range&.then { swagger_i18n("date_time_iso8601_range_min_range", min_range: _1.inspect) },
|
102
|
+
max_range&.then { swagger_i18n("date_time_iso8601_range_max_range", max_range: _1.inspect) },
|
103
|
+
]
|
104
|
+
}
|
107
105
|
end
|
108
106
|
end
|
@@ -54,16 +54,14 @@ class Explicit::Type::DateTimeUnixEpoch < Explicit::Type
|
|
54
54
|
end
|
55
55
|
end
|
56
56
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
})
|
67
|
-
end
|
57
|
+
def json_schema(flavour)
|
58
|
+
{
|
59
|
+
type: "integer",
|
60
|
+
minimum: 1,
|
61
|
+
format: "POSIX time",
|
62
|
+
description_topics: [
|
63
|
+
swagger_i18n("date_time_unix_epoch")
|
64
|
+
]
|
65
|
+
}
|
68
66
|
end
|
69
67
|
end
|
data/lib/explicit/type/enum.rb
CHANGED
@@ -29,12 +29,10 @@ class Explicit::Type::Enum < Explicit::Type
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
})
|
38
|
-
end
|
32
|
+
def json_schema(flavour)
|
33
|
+
{
|
34
|
+
type: "string",
|
35
|
+
enum: allowed_values
|
36
|
+
}
|
39
37
|
end
|
40
38
|
end
|
data/lib/explicit/type/file.rb
CHANGED
@@ -45,16 +45,14 @@ class Explicit::Type::File < Explicit::Type
|
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
})
|
58
|
-
end
|
48
|
+
def json_schema(flavour)
|
49
|
+
{
|
50
|
+
type: "string",
|
51
|
+
format: "binary",
|
52
|
+
description_topics: [
|
53
|
+
max_size&.then { swagger_i18n("file_max_size", max_size: number_to_human_size(_1)) },
|
54
|
+
content_types.any? ? swagger_i18n("file_content_types", content_types: content_types.join(', ')) : nil
|
55
|
+
]
|
56
|
+
}
|
59
57
|
end
|
60
58
|
end
|
data/lib/explicit/type/float.rb
CHANGED
@@ -69,19 +69,17 @@ class Explicit::Type::Float < Explicit::Type
|
|
69
69
|
end
|
70
70
|
end
|
71
71
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
}.compact_blank)
|
85
|
-
end
|
72
|
+
def json_schema(flavour)
|
73
|
+
{
|
74
|
+
type: "number",
|
75
|
+
minimum: min,
|
76
|
+
maximum: max,
|
77
|
+
description_topics: [
|
78
|
+
positive == false ? swagger_i18n("number_not_positive") : nil,
|
79
|
+
positive == true ? swagger_i18n("number_only_positive") : nil,
|
80
|
+
negative == false ? swagger_i18n("number_not_negative") : nil,
|
81
|
+
negative == true ? swagger_i18n("number_only_negative") : nil
|
82
|
+
].compact_blank
|
83
|
+
}.compact_blank
|
86
84
|
end
|
87
85
|
end
|
data/lib/explicit/type/hash.rb
CHANGED
@@ -46,15 +46,13 @@ class Explicit::Type::Hash < Explicit::Type
|
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
})
|
58
|
-
end
|
49
|
+
def json_schema(flavour)
|
50
|
+
{
|
51
|
+
type: "object",
|
52
|
+
additionalProperties: value_type.mcp_schema,
|
53
|
+
description_topics: [
|
54
|
+
empty == false ? swagger_i18n("hash_not_empty") : nil
|
55
|
+
]
|
56
|
+
}
|
59
57
|
end
|
60
58
|
end
|
@@ -69,19 +69,17 @@ class Explicit::Type::Integer < Explicit::Type
|
|
69
69
|
end
|
70
70
|
end
|
71
71
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
}.compact_blank)
|
85
|
-
end
|
72
|
+
def json_schema(flavour)
|
73
|
+
{
|
74
|
+
type: "integer",
|
75
|
+
minimum: min,
|
76
|
+
maximum: max,
|
77
|
+
description_topics: [
|
78
|
+
positive == false ? swagger_i18n("number_not_positive") : nil,
|
79
|
+
positive == true ? swagger_i18n("number_only_positive") : nil,
|
80
|
+
negative == false ? swagger_i18n("number_not_negative") : nil,
|
81
|
+
negative == true ? swagger_i18n("number_only_negative") : nil
|
82
|
+
].compact_blank
|
83
|
+
}.compact_blank
|
86
84
|
end
|
87
85
|
end
|
@@ -33,12 +33,10 @@ class Explicit::Type::Literal < Explicit::Type
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
})
|
42
|
-
end
|
36
|
+
def json_schema(flavour)
|
37
|
+
{
|
38
|
+
type: @value.is_a?(::String) ? "string" : "integer",
|
39
|
+
enum: [@value]
|
40
|
+
}
|
43
41
|
end
|
44
42
|
end
|
data/lib/explicit/type/one_of.rb
CHANGED
@@ -101,11 +101,9 @@ class Explicit::Type::OneOf < Explicit::Type
|
|
101
101
|
end
|
102
102
|
end
|
103
103
|
|
104
|
-
|
105
|
-
|
106
|
-
return subtypes.first.swagger_schema if subtypes.one?
|
104
|
+
def json_schema(flavour)
|
105
|
+
raise ::NotImplementedError if flavour == :mcp
|
107
106
|
|
108
|
-
|
109
|
-
end
|
107
|
+
{ oneOf: subtypes.map(&:swagger_schema) }
|
110
108
|
end
|
111
109
|
end
|
data/lib/explicit/type/record.rb
CHANGED
@@ -73,33 +73,20 @@ class Explicit::Type::Record < Explicit::Type
|
|
73
73
|
end
|
74
74
|
end
|
75
75
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
{
|
80
|
-
name:,
|
81
|
-
in: type.param_location_path? ? "path" : "body",
|
82
|
-
description: type.description,
|
83
|
-
required: !type.nilable,
|
84
|
-
schema: type.swagger_schema
|
85
|
-
}
|
86
|
-
end
|
76
|
+
def json_schema(flavour)
|
77
|
+
properties = attributes.to_h do |name, type|
|
78
|
+
[ name, flavour == :mcp ? type.mcp_schema : type.swagger_schema ]
|
87
79
|
end
|
88
80
|
|
89
|
-
|
90
|
-
|
91
|
-
[ name, type.swagger_schema ]
|
92
|
-
end
|
93
|
-
|
94
|
-
required = attributes.filter_map do |name, type|
|
95
|
-
type.required? ? name.to_s : nil
|
96
|
-
end
|
97
|
-
|
98
|
-
merge_base_swagger_schema({
|
99
|
-
type: "object",
|
100
|
-
properties:,
|
101
|
-
required:
|
102
|
-
}.compact_blank)
|
81
|
+
required = attributes.filter_map do |name, type|
|
82
|
+
type.required? ? name.to_s : nil
|
103
83
|
end
|
84
|
+
|
85
|
+
{
|
86
|
+
type: "object",
|
87
|
+
properties:,
|
88
|
+
required:,
|
89
|
+
additionalProperties: false
|
90
|
+
}.compact
|
104
91
|
end
|
105
92
|
end
|
data/lib/explicit/type/string.rb
CHANGED
@@ -51,18 +51,16 @@ class Explicit::Type::String < Explicit::Type
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
}.compact_blank)
|
66
|
-
end
|
54
|
+
def json_schema(flavour)
|
55
|
+
{
|
56
|
+
type: "string",
|
57
|
+
pattern: format&.inspect&.then { _1[1..-2] },
|
58
|
+
minLength: min_length || (empty == false ? 1 : nil),
|
59
|
+
maxLength: max_length,
|
60
|
+
description_topics: [
|
61
|
+
empty == false ? swagger_i18n("string_not_empty") : nil,
|
62
|
+
downcase == true ? swagger_i18n("string_downcase") : nil
|
63
|
+
].compact_blank
|
64
|
+
}.compact_blank
|
67
65
|
end
|
68
66
|
end
|
data/lib/explicit/type.rb
CHANGED
@@ -130,7 +130,7 @@ class Explicit::Type
|
|
130
130
|
end
|
131
131
|
|
132
132
|
def required?
|
133
|
-
!nilable
|
133
|
+
!nilable && default.blank?
|
134
134
|
end
|
135
135
|
|
136
136
|
def error_i18n(name, context = {})
|
@@ -156,7 +156,15 @@ class Explicit::Type
|
|
156
156
|
end
|
157
157
|
end
|
158
158
|
|
159
|
-
def
|
159
|
+
def swagger_schema
|
160
|
+
merge_base_json_schema(json_schema(:swagger))
|
161
|
+
end
|
162
|
+
|
163
|
+
def mcp_schema
|
164
|
+
merge_base_json_schema(json_schema(:mcp))
|
165
|
+
end
|
166
|
+
|
167
|
+
def merge_base_json_schema(attributes)
|
160
168
|
topics = attributes.delete(:description_topics)&.compact_blank || []
|
161
169
|
|
162
170
|
formatted_description =
|
data/lib/explicit/version.rb
CHANGED
data/lib/explicit.rb
CHANGED
@@ -4,6 +4,13 @@ require "explicit/configuration"
|
|
4
4
|
require "explicit/engine"
|
5
5
|
require "explicit/version"
|
6
6
|
|
7
|
+
require "explicit/mcp_server"
|
8
|
+
require "explicit/mcp_server/builder"
|
9
|
+
require "explicit/mcp_server/request"
|
10
|
+
require "explicit/mcp_server/router"
|
11
|
+
require "explicit/mcp_server/response"
|
12
|
+
require "explicit/mcp_server/tool"
|
13
|
+
|
7
14
|
require "explicit/documentation"
|
8
15
|
require "explicit/documentation/builder"
|
9
16
|
require "explicit/documentation/markdown"
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: explicit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.17
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Luiz Vasconcellos
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: rails
|
@@ -85,6 +85,12 @@ files:
|
|
85
85
|
- lib/explicit/documentation/page/request.rb
|
86
86
|
- lib/explicit/documentation/section.rb
|
87
87
|
- lib/explicit/engine.rb
|
88
|
+
- lib/explicit/mcp_server.rb
|
89
|
+
- lib/explicit/mcp_server/builder.rb
|
90
|
+
- lib/explicit/mcp_server/request.rb
|
91
|
+
- lib/explicit/mcp_server/response.rb
|
92
|
+
- lib/explicit/mcp_server/router.rb
|
93
|
+
- lib/explicit/mcp_server/tool.rb
|
88
94
|
- lib/explicit/request.rb
|
89
95
|
- lib/explicit/request/example.rb
|
90
96
|
- lib/explicit/request/invalid_params_error.rb
|
@@ -141,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
147
|
- !ruby/object:Gem::Version
|
142
148
|
version: '0'
|
143
149
|
requirements: []
|
144
|
-
rubygems_version: 3.6.
|
150
|
+
rubygems_version: 3.6.7
|
145
151
|
specification_version: 4
|
146
152
|
summary: Explicit is a validation and documentation library for REST APIs
|
147
153
|
test_files: []
|