textus 0.54.0 → 0.54.1
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/CHANGELOG.md +18 -0
- data/README.md +53 -73
- data/lib/textus/boot.rb +8 -1
- data/lib/textus/container.rb +21 -5
- data/lib/textus/core/freshness/verdict.rb +1 -1
- data/lib/textus/doctor/check/protocol_version.rb +6 -6
- data/lib/textus/doctor/check/stale_reviewed_stamp.rb +54 -0
- data/lib/textus/doctor.rb +1 -0
- data/lib/textus/envelope.rb +19 -6
- data/lib/textus/errors.rb +1 -1
- data/lib/textus/init.rb +4 -4
- data/lib/textus/manifest/schema/contract.rb +61 -0
- data/lib/textus/manifest/schema/semantics.rb +232 -0
- data/lib/textus/manifest/schema/validator.rb +24 -294
- data/lib/textus/manifest/schema.rb +1 -4
- data/lib/textus/manifest.rb +1 -3
- data/lib/textus/produce/render.rb +4 -2
- data/lib/textus/session.rb +14 -7
- data/lib/textus/store.rb +4 -4
- data/lib/textus/surfaces/mcp/catalog.rb +22 -10
- data/lib/textus/surfaces/mcp/server.rb +73 -135
- data/lib/textus/types.rb +15 -0
- data/lib/textus/version.rb +2 -2
- data/lib/textus/workflow/runner.rb +15 -0
- metadata +36 -7
- data/lib/textus/surfaces/mcp/routing.rb +0 -51
- data/lib/textus/surfaces/mcp/session.rb +0 -9
- data/lib/textus/surfaces/mcp/tool_schemas.rb +0 -17
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Surfaces
|
|
3
3
|
module MCP
|
|
4
|
-
# Derives the entire MCP tool surface from the per-verb contracts
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# return value with the contract's default view. No per-tool code.
|
|
4
|
+
# Derives the entire MCP tool surface from the per-verb contracts (ADR 0039).
|
|
5
|
+
# `build_tools` builds MCP::Tool instances for the SDK; `call` is the generic
|
|
6
|
+
# dispatch: map JSON args -> (positional, keyword) per the contract, invoke
|
|
7
|
+
# the verb through the role scope, then shape the return value. No per-tool code.
|
|
9
8
|
module Catalog
|
|
10
9
|
module_function
|
|
11
10
|
|
|
@@ -24,14 +23,27 @@ module Textus
|
|
|
24
23
|
.map(&:contract)
|
|
25
24
|
end
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
# Builds MCP::Tool instances for the SDK, bound to mcp_server.dispatch.
|
|
27
|
+
def build_tools(mcp_server)
|
|
28
|
+
Textus::Action::VERBS
|
|
29
|
+
.select { |_, klass| mcp_surfaced?(klass) }
|
|
30
|
+
.map do |name, action|
|
|
31
|
+
schema = action.contract.input_schema
|
|
32
|
+
schema = schema.reject { |k, v| k == :required && Array(v).empty? }
|
|
33
|
+
::MCP::Tool.define(
|
|
34
|
+
name: name.to_s,
|
|
35
|
+
description: action.contract.summary,
|
|
36
|
+
input_schema: schema,
|
|
37
|
+
) do |server_context:, **args|
|
|
38
|
+
mcp_server.dispatch(name, args, server_context)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
31
41
|
end
|
|
32
42
|
|
|
33
43
|
def names
|
|
34
|
-
|
|
44
|
+
Textus::Action::VERBS
|
|
45
|
+
.select { |_, klass| mcp_surfaced?(klass) }
|
|
46
|
+
.keys.map(&:to_s)
|
|
35
47
|
end
|
|
36
48
|
|
|
37
49
|
# MCP-surfaced read verbs, by Dispatcher class namespace — the agent's
|
|
@@ -1,161 +1,108 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
3
4
|
|
|
4
5
|
module Textus
|
|
5
6
|
module Surfaces
|
|
6
7
|
module MCP
|
|
7
|
-
#
|
|
8
|
-
#
|
|
8
|
+
# MCP stdio server backed by the official mcp gem. The SDK owns protocol
|
|
9
|
+
# negotiation, tool dispatch, and JSON-RPC framing. This class owns the
|
|
10
|
+
# textus Session lifecycle (built lazily on first tool call) and delegates
|
|
11
|
+
# execution to Catalog.
|
|
9
12
|
class Server
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@
|
|
18
|
-
@stdin = stdin
|
|
19
|
-
@stdout = stdout
|
|
20
|
-
@role = role
|
|
21
|
-
@session = nil
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def run
|
|
25
|
-
@stdin.each_line do |line|
|
|
26
|
-
line = line.strip
|
|
27
|
-
next if line.empty?
|
|
28
|
-
|
|
29
|
-
handle_line(line)
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
def handle_line(line)
|
|
36
|
-
return reject_oversized(line) if line.bytesize > MAX_LINE_BYTES
|
|
37
|
-
|
|
38
|
-
parse_and_dispatch(line)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def reject_oversized(line)
|
|
42
|
-
emit_error(nil, -32_700, "message too large (#{line.bytesize} bytes, limit #{MAX_LINE_BYTES})")
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def parse_and_dispatch(line)
|
|
46
|
-
dispatch(JSON.parse(line))
|
|
47
|
-
rescue JSON::ParserError => e
|
|
48
|
-
emit_error(nil, -32_700, "parse error: #{e.message}")
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def handle_initialize(rid, _params)
|
|
52
|
-
@session = build_session
|
|
53
|
-
emit_result(rid, {
|
|
54
|
-
"protocolVersion" => PROTOCOL_VERSION,
|
|
55
|
-
"serverInfo" => SERVER_INFO,
|
|
56
|
-
"capabilities" => { "tools" => {}, "resources" => {} },
|
|
57
|
-
})
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def build_session
|
|
61
|
-
# The acting role IS the resolved connection role (ADR 0040): the MCP
|
|
62
|
-
# transport defaults to `agent`, which can write the queue, so its
|
|
63
|
-
# propose_lane resolves directly. If a connection's role cannot propose,
|
|
64
|
-
# propose_lane is nil and the `propose` tool reports that honestly.
|
|
65
|
-
Session.new(
|
|
13
|
+
def initialize(store:, role: Textus::Role::DEFAULT, stdin: $stdin, stdout: $stdout)
|
|
14
|
+
@store = store
|
|
15
|
+
@role = role
|
|
16
|
+
@stdin = stdin
|
|
17
|
+
@stdout = stdout
|
|
18
|
+
# Session built eagerly so the contract_etag is captured at server start.
|
|
19
|
+
# Changes to manifest/hooks/schemas after this point are detected as drift.
|
|
20
|
+
@session = Textus::Session.new(
|
|
66
21
|
role: @role,
|
|
67
22
|
cursor: @store.audit_log.latest_seq,
|
|
68
23
|
propose_lane: @store.manifest.policy.propose_lane_for(@role),
|
|
69
|
-
contract_etag:
|
|
24
|
+
contract_etag: contract_etag_now,
|
|
70
25
|
)
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def handle_tools_list(rid)
|
|
74
|
-
emit_result(rid, { "tools" => Catalog.tool_schemas })
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def handle_tools_call(rid, params)
|
|
78
|
-
return unless session_ready?(rid)
|
|
79
26
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
27
|
+
@sdk = ::MCP::Server.new(
|
|
28
|
+
name: "textus",
|
|
29
|
+
version: Textus::VERSION,
|
|
30
|
+
tools: Catalog.build_tools(self),
|
|
31
|
+
resources: build_resources,
|
|
32
|
+
server_context: { mcp_server: self },
|
|
33
|
+
)
|
|
34
|
+
@sdk.resources_read_handler { |params, server_context:| handle_resource_read(params[:uri].to_s, server_context) }
|
|
85
35
|
end
|
|
86
36
|
|
|
87
|
-
|
|
88
|
-
|
|
37
|
+
# Runs the stdio line loop; delegates each JSON line to the SDK.
|
|
38
|
+
def run
|
|
39
|
+
@stdin.each_line do |line|
|
|
40
|
+
line = line.strip
|
|
41
|
+
next if line.empty?
|
|
89
42
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
end
|
|
43
|
+
response = @sdk.handle_json(line)
|
|
44
|
+
next unless response
|
|
93
45
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
result = Catalog.call(name, session: @session, store: @store, args: args)
|
|
98
|
-
update_session_for(name)
|
|
99
|
-
emit_tool_result(rid, result)
|
|
46
|
+
@stdout.puts(response)
|
|
47
|
+
@stdout.flush
|
|
48
|
+
end
|
|
100
49
|
end
|
|
101
50
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
51
|
+
# Called from every MCP::Tool handler block in Catalog.
|
|
52
|
+
# The SDK parses JSON with symbolize_names: true — all nested keys are symbols.
|
|
53
|
+
# Deep-stringify so Catalog.call receives the string-key format it expects.
|
|
54
|
+
def dispatch(verb_name, args, _server_context)
|
|
55
|
+
str_args = deep_stringify_keys(args)
|
|
56
|
+
@session.check_etag!(contract_etag_now) unless Catalog.read_verbs.include?(verb_name.to_s)
|
|
57
|
+
result = Catalog.call(verb_name.to_s, session: @session, store: @store, args: str_args)
|
|
58
|
+
update_session_for(verb_name.to_s)
|
|
59
|
+
::MCP::Tool::Response.new([{ type: "text", text: JSON.dump(result) }])
|
|
60
|
+
rescue Textus::ContractDrift => e
|
|
61
|
+
raise_handler_error(e.message, Textus::ContractDrift::JSONRPC_CODE)
|
|
62
|
+
rescue CursorExpired => e
|
|
63
|
+
raise_handler_error(e.message, CursorExpired::JSONRPC_CODE)
|
|
64
|
+
rescue Textus::Surfaces::MCP::ToolError => e
|
|
65
|
+
raise_handler_error(e.message, ToolError::JSONRPC_CODE)
|
|
66
|
+
rescue StandardError => e
|
|
67
|
+
raise_handler_error("internal: #{e.class}: #{e.message}", -32_603)
|
|
105
68
|
end
|
|
106
69
|
|
|
107
|
-
|
|
108
|
-
emit_result(rid, {
|
|
109
|
-
"content" => [{ "type" => "text", "text" => JSON.dump(result) }],
|
|
110
|
-
"isError" => false,
|
|
111
|
-
})
|
|
112
|
-
end
|
|
70
|
+
private
|
|
113
71
|
|
|
114
|
-
def
|
|
115
|
-
|
|
72
|
+
def update_session_for(verb_name)
|
|
73
|
+
@session = @session.advance_cursor(@store.audit_log.latest_seq) if verb_name == "pulse"
|
|
74
|
+
@session = @session.with(contract_etag: contract_etag_now) if verb_name == "boot"
|
|
116
75
|
end
|
|
117
76
|
|
|
118
|
-
def
|
|
77
|
+
def build_resources
|
|
119
78
|
machine_lane = @store.manifest.policy.machine_lane
|
|
120
79
|
return [] unless machine_lane
|
|
121
80
|
|
|
122
|
-
produced_entries(machine_lane).map { |e| resource_descriptor(e) }
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def produced_entries(machine_lane)
|
|
126
81
|
@store.manifest.data.entries
|
|
127
82
|
.select { |e| e.lane == machine_lane && e.is_a?(Textus::Manifest::Entry::Produced) }
|
|
83
|
+
.map { |e| ::MCP::Resource.new(uri: "textus://#{e.key.tr(".", "/")}", name: e.key, mime_type: mime_for_format(e.format)) }
|
|
128
84
|
end
|
|
129
85
|
|
|
130
|
-
def
|
|
131
|
-
{
|
|
132
|
-
"uri" => "textus://#{entry.key.tr(".", "/")}",
|
|
133
|
-
"name" => entry.key,
|
|
134
|
-
"mimeType" => mime_for_format(entry.format),
|
|
135
|
-
}
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def handle_resources_read(rid, params)
|
|
139
|
-
uri = params["uri"].to_s
|
|
86
|
+
def handle_resource_read(uri, _server_context)
|
|
140
87
|
key = uri.delete_prefix("textus://").tr("/", ".")
|
|
141
|
-
emit_result(rid, resource_contents(uri, key))
|
|
142
|
-
rescue Textus::Error => e
|
|
143
|
-
emit_error(rid, ToolError::JSONRPC_CODE, "resource read failed: #{e.message}")
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def resource_contents(uri, key)
|
|
147
88
|
env = @store.as(@role).get(key)
|
|
148
|
-
text =
|
|
89
|
+
text = env.content.is_a?(Hash) ? JSON.dump(env.content) : (env.body || "").to_s
|
|
149
90
|
mime = mime_for_format(@store.manifest.resolver.resolve(key).entry.format)
|
|
150
|
-
|
|
91
|
+
[{ uri: uri, mimeType: mime, text: text }]
|
|
92
|
+
rescue Textus::Error => e
|
|
93
|
+
raise_handler_error("resource read failed: #{e.message}", -32_603)
|
|
151
94
|
end
|
|
152
95
|
|
|
153
|
-
def
|
|
154
|
-
content.is_a?(Hash) ? JSON.dump(content) : content.to_s
|
|
155
|
-
end
|
|
96
|
+
def contract_etag_now = Textus::Etag.for_contract(@store.root)
|
|
156
97
|
|
|
157
|
-
|
|
158
|
-
|
|
98
|
+
# The SDK parses JSON with symbolize_names:true, making all nested hash keys symbols.
|
|
99
|
+
# Recursively stringify so Catalog.call receives string-keyed hashes throughout.
|
|
100
|
+
def deep_stringify_keys(obj)
|
|
101
|
+
case obj
|
|
102
|
+
when Hash then obj.transform_keys(&:to_s).transform_values { |v| deep_stringify_keys(v) }
|
|
103
|
+
when Array then obj.map { |v| deep_stringify_keys(v) }
|
|
104
|
+
else obj
|
|
105
|
+
end
|
|
159
106
|
end
|
|
160
107
|
|
|
161
108
|
def mime_for_format(format)
|
|
@@ -166,17 +113,8 @@ module Textus
|
|
|
166
113
|
end
|
|
167
114
|
end
|
|
168
115
|
|
|
169
|
-
def
|
|
170
|
-
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
def emit_error(rid, code, message)
|
|
174
|
-
write({ "jsonrpc" => "2.0", "id" => rid, "error" => { "code" => code, "message" => message } })
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def write(obj)
|
|
178
|
-
@stdout.puts(JSON.dump(obj))
|
|
179
|
-
@stdout.flush
|
|
116
|
+
def raise_handler_error(message, code)
|
|
117
|
+
raise ::MCP::Server::RequestHandlerError.new(message, nil, error_code: code)
|
|
180
118
|
end
|
|
181
119
|
end
|
|
182
120
|
end
|
data/lib/textus/types.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/types"
|
|
4
|
+
|
|
5
|
+
module Textus
|
|
6
|
+
module Types
|
|
7
|
+
include Dry.Types()
|
|
8
|
+
|
|
9
|
+
RoleName = Types::String.constrained(included_in: Textus::Role::NAMES)
|
|
10
|
+
Cursor = Types::Integer.constrained(gteq: 0)
|
|
11
|
+
FormatName = Types::String.constrained(
|
|
12
|
+
included_in: %w[markdown json yaml text], # must match Format::STRATEGIES.keys
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/textus/version.rb
CHANGED
|
@@ -64,6 +64,21 @@ module Textus
|
|
|
64
64
|
mentry: ctx.entry,
|
|
65
65
|
payload: Envelope::Writer::Payload.new(**normalized),
|
|
66
66
|
)
|
|
67
|
+
publish_external(key, ctx)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def publish_external(key, ctx)
|
|
71
|
+
entry = ctx.entry
|
|
72
|
+
return unless entry.publish_tree || !Array(entry.publish_to).empty?
|
|
73
|
+
|
|
74
|
+
entry_path = @container.manifest.resolver.resolve(key).path
|
|
75
|
+
return unless entry.publish_tree || File.exist?(entry_path)
|
|
76
|
+
|
|
77
|
+
reader = Textus::Envelope::Reader.from(container: @container)
|
|
78
|
+
pctx = Textus::Manifest::Entry::Base::PublishContext.new(
|
|
79
|
+
container: @container, call: @call, reader: reader.method(:read),
|
|
80
|
+
)
|
|
81
|
+
entry.publish_via(pctx)
|
|
67
82
|
end
|
|
68
83
|
|
|
69
84
|
def normalize(data, format)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: textus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.54.
|
|
4
|
+
version: 0.54.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -24,19 +24,47 @@ dependencies:
|
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '3.0'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name:
|
|
27
|
+
name: dry-schema
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '1.
|
|
32
|
+
version: '1.13'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '1.
|
|
39
|
+
version: '1.13'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: dry-struct
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.6'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.6'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: mcp
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.20'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.20'
|
|
40
68
|
- !ruby/object:Gem::Dependency
|
|
41
69
|
name: psych
|
|
42
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -191,6 +219,7 @@ files:
|
|
|
191
219
|
- lib/textus/doctor/check/schema_violations.rb
|
|
192
220
|
- lib/textus/doctor/check/schemas.rb
|
|
193
221
|
- lib/textus/doctor/check/sentinels.rb
|
|
222
|
+
- lib/textus/doctor/check/stale_reviewed_stamp.rb
|
|
194
223
|
- lib/textus/doctor/check/templates.rb
|
|
195
224
|
- lib/textus/doctor/check/unowned_schema_fields.rb
|
|
196
225
|
- lib/textus/doctor/validator.rb
|
|
@@ -251,7 +280,9 @@ files:
|
|
|
251
280
|
- lib/textus/manifest/resolver.rb
|
|
252
281
|
- lib/textus/manifest/rules.rb
|
|
253
282
|
- lib/textus/manifest/schema.rb
|
|
283
|
+
- lib/textus/manifest/schema/contract.rb
|
|
254
284
|
- lib/textus/manifest/schema/keys.rb
|
|
285
|
+
- lib/textus/manifest/schema/semantics.rb
|
|
255
286
|
- lib/textus/manifest/schema/validator.rb
|
|
256
287
|
- lib/textus/manifest/schema/vocabulary.rb
|
|
257
288
|
- lib/textus/ports/audit_log.rb
|
|
@@ -293,12 +324,10 @@ files:
|
|
|
293
324
|
- lib/textus/surfaces/mcp.rb
|
|
294
325
|
- lib/textus/surfaces/mcp/catalog.rb
|
|
295
326
|
- lib/textus/surfaces/mcp/errors.rb
|
|
296
|
-
- lib/textus/surfaces/mcp/routing.rb
|
|
297
327
|
- lib/textus/surfaces/mcp/server.rb
|
|
298
|
-
- lib/textus/surfaces/mcp/session.rb
|
|
299
|
-
- lib/textus/surfaces/mcp/tool_schemas.rb
|
|
300
328
|
- lib/textus/surfaces/role_scope.rb
|
|
301
329
|
- lib/textus/surfaces/watcher.rb
|
|
330
|
+
- lib/textus/types.rb
|
|
302
331
|
- lib/textus/uid.rb
|
|
303
332
|
- lib/textus/version.rb
|
|
304
333
|
- lib/textus/workflow.rb
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Surfaces
|
|
3
|
-
module MCP
|
|
4
|
-
# Protocol routing for the MCP JSON-RPC server. Mixed into Server so all
|
|
5
|
-
# handle_* and emit_* methods are in scope without exposing them publicly.
|
|
6
|
-
module Routing
|
|
7
|
-
TOOL_METHODS = %w[initialize tools/list tools/call].freeze
|
|
8
|
-
RESOURCE_METHODS = %w[resources/list resources/read].freeze
|
|
9
|
-
|
|
10
|
-
def dispatch(msg)
|
|
11
|
-
rid = msg["id"]
|
|
12
|
-
params = msg["params"] || {}
|
|
13
|
-
route(msg["method"], rid, params)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
private
|
|
17
|
-
|
|
18
|
-
def route(method, rid, params)
|
|
19
|
-
return route_tool(method, rid, params) if TOOL_METHODS.include?(method)
|
|
20
|
-
return route_resource(method, rid, params) if RESOURCE_METHODS.include?(method)
|
|
21
|
-
|
|
22
|
-
route_protocol(method, rid)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def route_tool(method, rid, params)
|
|
26
|
-
case method
|
|
27
|
-
when "initialize" then handle_initialize(rid, params)
|
|
28
|
-
when "tools/list" then handle_tools_list(rid)
|
|
29
|
-
when "tools/call" then handle_tools_call(rid, params)
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def route_resource(method, rid, params)
|
|
34
|
-
case method
|
|
35
|
-
when "resources/list" then handle_resources_list(rid)
|
|
36
|
-
when "resources/read" then handle_resources_read(rid, params)
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def route_protocol(method, rid)
|
|
41
|
-
case method
|
|
42
|
-
when "ping" then emit_result(rid, {})
|
|
43
|
-
when "shutdown" then emit_result(rid, nil)
|
|
44
|
-
when "notifications/initialized" then nil
|
|
45
|
-
else emit_error(rid, -32_601, "method not found: #{method}")
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Surfaces
|
|
3
|
-
module MCP
|
|
4
|
-
# Kept for name stability (ADR 0039). The JSON schemas are DERIVED from
|
|
5
|
-
# per-verb contracts; this delegates to MCP::Catalog. The hand-written
|
|
6
|
-
# array is gone — a kwarg rename now updates the schema automatically (and
|
|
7
|
-
# the signature guard fails if the contract lags the use-case).
|
|
8
|
-
module ToolSchemas
|
|
9
|
-
module_function
|
|
10
|
-
|
|
11
|
-
def all
|
|
12
|
-
Catalog.tool_schemas
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|