kairos-chain 3.14.1 → 3.15.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -0
- data/lib/kairos_mcp/anthropic_skill_parser.rb +9 -1
- data/lib/kairos_mcp/version.rb +1 -1
- data/templates/skillsets/mmp/lib/mmp/place_client.rb +64 -5
- data/templates/skillsets/mmp/tools/meeting_acquire_skill.rb +94 -78
- data/templates/skillsets/mmp/tools/meeting_browse.rb +29 -35
- data/templates/skillsets/mmp/tools/meeting_deposit.rb +23 -29
- data/templates/skillsets/mmp/tools/meeting_federate.rb +46 -94
- data/templates/skillsets/mmp/tools/meeting_get_skill_details.rb +64 -38
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: de33a990a4ade8e91533b4a596e0f9e73fcd921c68af62cc9daa30173879234a
|
|
4
|
+
data.tar.gz: 29930e999d710a29626a4a22ed8f49f66dafe2b36f675be1e04bd308a76c75ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3bb7aa9b8e4ac13aaa84b126277cdefb3ca479a0aee831e159da89881605061dd2f16aa2efdd11cc2c4be28736d295d90c65c6e74be88e41c7788ee000fc8341
|
|
7
|
+
data.tar.gz: b073833ea3eb15699e57078accac4eed96a3c30088382c9fa310cb1847f0bd6ea41392d1d9ea269ff9e1406d49ddd793b8f0b488bc6369144ce52b7c46812340
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,52 @@ All notable changes to the `kairos-chain` gem will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
This project follows [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [3.15.0] - 2026-04-15
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **PlaceClient browse drops tags** — `PlaceClient#browse` silently discarded the
|
|
12
|
+
`tags` parameter when building query params. Tag-filtered browse and federate
|
|
13
|
+
now work correctly.
|
|
14
|
+
- **Silent error swallowing in 5 MMP tools** — `meeting_browse`, `meeting_deposit`,
|
|
15
|
+
`meeting_get_skill_details`, `meeting_acquire_skill`, and `meeting_federate`
|
|
16
|
+
used raw `Net::HTTP` with `rescue StandardError; nil`, hiding connection errors,
|
|
17
|
+
auth failures, and server errors. All 5 tools now use `PlaceClient` with
|
|
18
|
+
structured error reporting.
|
|
19
|
+
- **Wrong auth token for /meeting/v1/* endpoints** — `meeting_get_skill_details`
|
|
20
|
+
and `meeting_acquire_skill` (peer path) sent the Place session token to
|
|
21
|
+
`/meeting/v1/*` endpoints instead of the meeting session token. Token routing
|
|
22
|
+
now matches endpoint prefix: `/place/v1/*` uses `session_token`,
|
|
23
|
+
`/meeting/v1/*` uses `meeting_session_token`.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **PlaceClient.reconnect** — class method for restoring client state from saved
|
|
28
|
+
connection without re-registration. Replaces `instance_variable_set` pattern.
|
|
29
|
+
- **PlaceClient new methods** — `deposit`, `get_skill_details`, `get_skill_content`,
|
|
30
|
+
`request_skill_content`, `place_info` (unauthenticated).
|
|
31
|
+
- **Response adapters** — A1 (Place skill content), A2 (peer skill content with
|
|
32
|
+
nested payload unwrap), A3 (skill details with metadata envelope unwrap and
|
|
33
|
+
error/not_found distinction).
|
|
34
|
+
- **Expanded network error handling** — `PlaceClient` now catches `SocketError`,
|
|
35
|
+
`Net::ReadTimeout`, `OpenSSL::SSL::SSLError`, `Errno::ECONNRESET`, `Errno::EPIPE`
|
|
36
|
+
in addition to `Errno::ECONNREFUSED` and `Net::OpenTimeout`.
|
|
37
|
+
- **PlaceClient.parse_response preserves server error details** — non-success HTTP
|
|
38
|
+
responses now retain full server body (`:reasons`, `:status`, etc.) instead of
|
|
39
|
+
wrapping into a generic `{ error: "HTTP 4xx" }`.
|
|
40
|
+
|
|
41
|
+
### Removed
|
|
42
|
+
|
|
43
|
+
- **Dead code** — `get_details_relay` and `get_skill_from_relay` (unused relay
|
|
44
|
+
path methods) removed from `meeting_get_skill_details` and `meeting_acquire_skill`.
|
|
45
|
+
|
|
46
|
+
### Review
|
|
47
|
+
|
|
48
|
+
- Design: 3 rounds × 3 LLMs (Claude Opus 4.6, Codex GPT-5.4, Cursor Composer-2)
|
|
49
|
+
- Implementation: 1 round × 3 LLMs
|
|
50
|
+
- Key findings: browse tags bug (3/3), response format contract (3/3),
|
|
51
|
+
token routing (Codex FAIL → resolved), adapter key mismatch (Claude HIGH → resolved)
|
|
52
|
+
|
|
7
53
|
## [3.14.1] - 2026-04-12
|
|
8
54
|
|
|
9
55
|
### Fixed
|
|
@@ -78,7 +78,7 @@ module KairosMcp
|
|
|
78
78
|
description: frontmatter['description'],
|
|
79
79
|
version: frontmatter['version'],
|
|
80
80
|
layer: frontmatter['layer'],
|
|
81
|
-
tags: frontmatter['tags']
|
|
81
|
+
tags: normalize_tags(frontmatter['tags']),
|
|
82
82
|
content: body.strip,
|
|
83
83
|
frontmatter: frontmatter,
|
|
84
84
|
base_path: skill_dir,
|
|
@@ -204,6 +204,14 @@ module KairosMcp
|
|
|
204
204
|
|
|
205
205
|
private
|
|
206
206
|
|
|
207
|
+
def normalize_tags(tags)
|
|
208
|
+
case tags
|
|
209
|
+
when Array then tags
|
|
210
|
+
when String then tags.split(',').map(&:strip).reject(&:empty?)
|
|
211
|
+
else []
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
207
215
|
def find_md_file(skill_dir)
|
|
208
216
|
# First try to find a file with the same name as the directory
|
|
209
217
|
skill_name = File.basename(skill_dir)
|
data/lib/kairos_mcp/version.rb
CHANGED
|
@@ -11,6 +11,12 @@ module MMP
|
|
|
11
11
|
DEFAULT_MAX_SESSION_MINUTES = 60
|
|
12
12
|
DEFAULT_WARN_AFTER_INTERACTIONS = 50
|
|
13
13
|
|
|
14
|
+
NETWORK_ERRORS = [
|
|
15
|
+
Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EPIPE,
|
|
16
|
+
Net::OpenTimeout, Net::ReadTimeout,
|
|
17
|
+
SocketError, OpenSSL::SSL::SSLError
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
14
20
|
def initialize(place_url:, identity:, timeout: 10, crypto: nil, keypair_path: nil, config: {})
|
|
15
21
|
@place_url = place_url.chomp('/')
|
|
16
22
|
@identity = identity
|
|
@@ -26,6 +32,15 @@ module MMP
|
|
|
26
32
|
@peer_public_keys = {}
|
|
27
33
|
end
|
|
28
34
|
|
|
35
|
+
# Reconnect using saved connection state (no re-registration)
|
|
36
|
+
def self.reconnect(place_url:, identity:, session_token:, agent_id: nil, timeout: 10, config: {})
|
|
37
|
+
client = new(place_url: place_url, identity: identity, timeout: timeout, config: config)
|
|
38
|
+
client.instance_variable_set(:@bearer_token, session_token)
|
|
39
|
+
client.instance_variable_set(:@agent_id, agent_id)
|
|
40
|
+
client.instance_variable_set(:@connected, true)
|
|
41
|
+
client
|
|
42
|
+
end
|
|
43
|
+
|
|
29
44
|
def connect
|
|
30
45
|
result = register
|
|
31
46
|
if result && result[:agent_id]
|
|
@@ -88,10 +103,36 @@ module MMP
|
|
|
88
103
|
end
|
|
89
104
|
|
|
90
105
|
def browse(type: nil, search: nil, tags: nil, limit: nil)
|
|
91
|
-
params = {}
|
|
106
|
+
params = {}
|
|
107
|
+
params[:type] = type if type
|
|
108
|
+
params[:search] = search if search
|
|
109
|
+
params[:tags] = tags.is_a?(Array) ? tags.join(',') : tags if tags
|
|
110
|
+
params[:limit] = limit if limit
|
|
92
111
|
get('/place/v1/board/browse', params)
|
|
93
112
|
end
|
|
94
113
|
|
|
114
|
+
def deposit(skill)
|
|
115
|
+
post('/place/v1/deposit', skill)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def get_skill_details(skill_id:)
|
|
119
|
+
get('/meeting/v1/skill_details', { skill_id: skill_id })
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def get_skill_content(skill_id:, owner: nil)
|
|
123
|
+
params = {}
|
|
124
|
+
params[:owner] = owner if owner
|
|
125
|
+
get("/place/v1/skill_content/#{URI.encode_www_form_component(skill_id)}", params)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def request_skill_content(skill_id:)
|
|
129
|
+
post('/meeting/v1/skill_content', { skill_id: skill_id })
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def place_info
|
|
133
|
+
get_unauthenticated('/place/v1/info')
|
|
134
|
+
end
|
|
135
|
+
|
|
95
136
|
def withdraw(skill_id:, reason:)
|
|
96
137
|
delete("/place/v1/deposit/#{URI.encode_www_form_component(skill_id)}", { reason: reason })
|
|
97
138
|
end
|
|
@@ -160,7 +201,20 @@ module MMP
|
|
|
160
201
|
response = http.request(req)
|
|
161
202
|
@interaction_count += 1
|
|
162
203
|
parse_response(response)
|
|
163
|
-
rescue
|
|
204
|
+
rescue *NETWORK_ERRORS => e
|
|
205
|
+
{ error: "Connection failed: #{e.message}" }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def get_unauthenticated(path, params = {})
|
|
209
|
+
uri = URI.parse("#{@place_url}#{path}")
|
|
210
|
+
uri.query = URI.encode_www_form(params) unless params.empty?
|
|
211
|
+
http = Net::HTTP.new(uri.host, uri.port); http.open_timeout = @timeout; http.read_timeout = @timeout
|
|
212
|
+
http.use_ssl = (uri.scheme == 'https')
|
|
213
|
+
req = Net::HTTP::Get.new(uri)
|
|
214
|
+
response = http.request(req)
|
|
215
|
+
@interaction_count += 1
|
|
216
|
+
parse_response(response)
|
|
217
|
+
rescue *NETWORK_ERRORS => e
|
|
164
218
|
{ error: "Connection failed: #{e.message}" }
|
|
165
219
|
end
|
|
166
220
|
|
|
@@ -186,15 +240,20 @@ module MMP
|
|
|
186
240
|
req.body = JSON.generate(body) if body
|
|
187
241
|
@interaction_count += 1
|
|
188
242
|
parse_response(http.request(req))
|
|
189
|
-
rescue
|
|
243
|
+
rescue *NETWORK_ERRORS => e
|
|
190
244
|
{ error: "Connection failed: #{e.message}" }
|
|
191
245
|
end
|
|
192
246
|
|
|
193
247
|
def parse_response(response)
|
|
194
248
|
data = JSON.parse(response.body, symbolize_names: true)
|
|
195
|
-
response.is_a?(Net::HTTPSuccess)
|
|
249
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
250
|
+
data
|
|
251
|
+
else
|
|
252
|
+
data[:error] ||= "HTTP #{response.code}"
|
|
253
|
+
data
|
|
254
|
+
end
|
|
196
255
|
rescue JSON::ParserError
|
|
197
|
-
{ error: "Invalid JSON response" }
|
|
256
|
+
{ error: "Invalid JSON response (HTTP #{response.code})" }
|
|
198
257
|
end
|
|
199
258
|
end
|
|
200
259
|
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'net/http'
|
|
4
|
-
require 'uri'
|
|
5
3
|
require 'json'
|
|
6
4
|
require 'yaml'
|
|
7
5
|
require 'digest'
|
|
@@ -52,11 +50,6 @@ module KairosMcp
|
|
|
52
50
|
owner_agent_id = arguments['owner_agent_id']
|
|
53
51
|
save_layer = arguments['save_to_layer'] || 'L1'
|
|
54
52
|
|
|
55
|
-
config = ::MMP.load_config
|
|
56
|
-
unless config['enabled']
|
|
57
|
-
return text_content(JSON.pretty_generate({ error: 'Meeting Protocol is disabled' }))
|
|
58
|
-
end
|
|
59
|
-
|
|
60
53
|
connection = load_connection_state
|
|
61
54
|
unless connection
|
|
62
55
|
return text_content(JSON.pretty_generate({ error: 'Not connected', hint: 'Use meeting_connect first' }))
|
|
@@ -64,22 +57,30 @@ module KairosMcp
|
|
|
64
57
|
|
|
65
58
|
begin
|
|
66
59
|
url = connection['url'] || connection[:url]
|
|
67
|
-
token = connection['session_token'] || connection[:session_token]
|
|
68
60
|
|
|
69
61
|
if peer_id
|
|
70
|
-
#
|
|
62
|
+
# Peer direct path: /meeting/v1/skill_content
|
|
71
63
|
peer = find_peer(connection, peer_id)
|
|
72
64
|
unless peer
|
|
73
65
|
return text_content(JSON.pretty_generate({ error: "Peer not found: #{peer_id}" }))
|
|
74
66
|
end
|
|
75
67
|
endpoint = peer['endpoint'] || peer[:endpoint]
|
|
76
68
|
target = endpoint || url
|
|
77
|
-
|
|
69
|
+
|
|
70
|
+
meeting_client = build_meeting_client(url_override: target)
|
|
71
|
+
return meeting_client if meeting_client.is_a?(Array)
|
|
72
|
+
|
|
73
|
+
raw = meeting_client.request_skill_content(skill_id: skill_id)
|
|
74
|
+
content_result = adapt_peer_skill_content(raw, skill_id)
|
|
78
75
|
source_id = peer_id
|
|
79
76
|
source_name = peer['name'] || peer[:name] || peer_id
|
|
80
77
|
else
|
|
81
|
-
#
|
|
82
|
-
|
|
78
|
+
# Place deposit path: /place/v1/skill_content/:id
|
|
79
|
+
place_client = build_place_client
|
|
80
|
+
return place_client if place_client.is_a?(Array)
|
|
81
|
+
|
|
82
|
+
raw = place_client.get_skill_content(skill_id: skill_id, owner: owner_agent_id)
|
|
83
|
+
content_result = adapt_place_skill_content(raw, skill_id)
|
|
83
84
|
source_id = content_result[:depositor_id] || owner_agent_id || 'place'
|
|
84
85
|
source_name = source_id
|
|
85
86
|
end
|
|
@@ -155,10 +156,89 @@ module KairosMcp
|
|
|
155
156
|
(connection['peers'] || connection[:peers] || []).find { |p| (p['agent_id'] || p[:agent_id]) == peer_id }
|
|
156
157
|
end
|
|
157
158
|
|
|
159
|
+
# Build PlaceClient with session_token for /place/v1/* endpoints
|
|
160
|
+
def build_place_client(timeout: 10)
|
|
161
|
+
config = ::MMP.load_config
|
|
162
|
+
unless config['enabled']
|
|
163
|
+
return text_content(JSON.pretty_generate({ error: 'Meeting Protocol is disabled' }))
|
|
164
|
+
end
|
|
165
|
+
connection = load_connection_state
|
|
166
|
+
unless connection
|
|
167
|
+
return text_content(JSON.pretty_generate({ error: 'Not connected', hint: 'Use meeting_connect first' }))
|
|
168
|
+
end
|
|
169
|
+
url = connection['url'] || connection[:url]
|
|
170
|
+
token = connection['session_token'] || connection[:session_token]
|
|
171
|
+
agent_id = connection['agent_id'] || connection[:agent_id]
|
|
172
|
+
identity = ::MMP::Identity.new(config: config)
|
|
173
|
+
::MMP::PlaceClient.reconnect(
|
|
174
|
+
place_url: url, identity: identity,
|
|
175
|
+
session_token: token, agent_id: agent_id, timeout: timeout
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Build PlaceClient with meeting_session_token for /meeting/v1/* endpoints
|
|
180
|
+
def build_meeting_client(url_override: nil, timeout: 10)
|
|
181
|
+
config = ::MMP.load_config
|
|
182
|
+
unless config['enabled']
|
|
183
|
+
return text_content(JSON.pretty_generate({ error: 'Meeting Protocol is disabled' }))
|
|
184
|
+
end
|
|
185
|
+
connection = load_connection_state
|
|
186
|
+
unless connection
|
|
187
|
+
return text_content(JSON.pretty_generate({ error: 'Not connected', hint: 'Use meeting_connect first' }))
|
|
188
|
+
end
|
|
189
|
+
url = url_override || connection['url'] || connection[:url]
|
|
190
|
+
token = connection['meeting_session_token'] || connection[:meeting_session_token]
|
|
191
|
+
unless token
|
|
192
|
+
return text_content(JSON.pretty_generate({
|
|
193
|
+
error: 'No meeting session token',
|
|
194
|
+
hint: 'meeting_connect may have failed the /meeting/v1/introduce handshake. Reconnect.'
|
|
195
|
+
}))
|
|
196
|
+
end
|
|
197
|
+
identity = ::MMP::Identity.new(config: config)
|
|
198
|
+
::MMP::PlaceClient.reconnect(
|
|
199
|
+
place_url: url, identity: identity,
|
|
200
|
+
session_token: token, timeout: timeout
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Adapter A1: Place deposit skill content
|
|
205
|
+
def adapt_place_skill_content(raw, skill_id)
|
|
206
|
+
if raw[:error]
|
|
207
|
+
{ success: false, error: raw[:error] }
|
|
208
|
+
else
|
|
209
|
+
{
|
|
210
|
+
success: true,
|
|
211
|
+
name: raw[:name] || skill_id,
|
|
212
|
+
skill_name: raw[:name] || skill_id,
|
|
213
|
+
format: raw[:format] || 'markdown',
|
|
214
|
+
content: raw[:content],
|
|
215
|
+
content_hash: raw[:content_hash],
|
|
216
|
+
size_bytes: raw[:content]&.bytesize || 0,
|
|
217
|
+
depositor_id: raw[:depositor_id],
|
|
218
|
+
trust_notice: raw[:trust_notice]
|
|
219
|
+
}
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Adapter A2: Peer direct skill content
|
|
224
|
+
def adapt_peer_skill_content(raw, skill_id)
|
|
225
|
+
if raw[:error]
|
|
226
|
+
{ success: false, error: raw[:error] }
|
|
227
|
+
else
|
|
228
|
+
payload = raw.dig(:message, :payload) || raw[:payload] || raw
|
|
229
|
+
{
|
|
230
|
+
success: true,
|
|
231
|
+
skill_name: payload[:skill_name] || skill_id,
|
|
232
|
+
format: payload[:format] || 'markdown',
|
|
233
|
+
content: payload[:content],
|
|
234
|
+
content_hash: payload[:content_hash],
|
|
235
|
+
size_bytes: payload[:content]&.bytesize || 0
|
|
236
|
+
}
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
158
240
|
MAX_TRUSTED_PEERS = 100
|
|
159
241
|
|
|
160
|
-
# Save or update a trusted peer in L1 knowledge.
|
|
161
|
-
# Returns true on success, false on failure.
|
|
162
242
|
def save_trusted_peer(agent_id:, name:, place_url:, skill_acquired:)
|
|
163
243
|
dir = File.join(KairosMcp.data_dir, 'knowledge', 'trusted_peers')
|
|
164
244
|
FileUtils.mkdir_p(dir)
|
|
@@ -167,7 +247,6 @@ module KairosMcp
|
|
|
167
247
|
|
|
168
248
|
peers = load_trusted_peers_data(filepath)
|
|
169
249
|
|
|
170
|
-
# Find or create peer entry
|
|
171
250
|
existing = peers.find { |p| p['agent_id'] == agent_id }
|
|
172
251
|
if existing
|
|
173
252
|
existing['name'] = name
|
|
@@ -189,7 +268,6 @@ module KairosMcp
|
|
|
189
268
|
}
|
|
190
269
|
end
|
|
191
270
|
|
|
192
|
-
# LRU eviction: keep most recently interacted peers
|
|
193
271
|
if peers.size > MAX_TRUSTED_PEERS
|
|
194
272
|
peers = peers.sort_by { |p| p['last_interaction'] || '' }.last(MAX_TRUSTED_PEERS)
|
|
195
273
|
end
|
|
@@ -222,68 +300,6 @@ module KairosMcp
|
|
|
222
300
|
content = "---\n#{frontmatter.to_yaml}---\n\n# Trusted Peers\n\nPeers from whom skills were successfully acquired.\nAuto-managed by meeting_acquire_skill. Max #{MAX_TRUSTED_PEERS} entries (LRU).\n"
|
|
223
301
|
File.write(filepath, content)
|
|
224
302
|
end
|
|
225
|
-
|
|
226
|
-
# Acquire from Place's deposited skills (A案)
|
|
227
|
-
def get_skill_from_place(url, skill_id, owner_agent_id: nil, bearer_token: nil)
|
|
228
|
-
path = "/place/v1/skill_content/#{URI.encode_www_form_component(skill_id)}"
|
|
229
|
-
path += "?owner=#{URI.encode_www_form_component(owner_agent_id)}" if owner_agent_id
|
|
230
|
-
uri = URI.parse("#{url}#{path}")
|
|
231
|
-
http = Net::HTTP.new(uri.host, uri.port); http.use_ssl = (uri.scheme == 'https')
|
|
232
|
-
http.open_timeout = 5; http.read_timeout = 10
|
|
233
|
-
req = Net::HTTP::Get.new(uri)
|
|
234
|
-
req['Authorization'] = "Bearer #{bearer_token}" if bearer_token
|
|
235
|
-
response = http.request(req)
|
|
236
|
-
if response.is_a?(Net::HTTPSuccess)
|
|
237
|
-
data = JSON.parse(response.body, symbolize_names: true)
|
|
238
|
-
{
|
|
239
|
-
success: true,
|
|
240
|
-
skill_name: data[:name] || skill_id,
|
|
241
|
-
format: data[:format] || 'markdown',
|
|
242
|
-
content: data[:content],
|
|
243
|
-
content_hash: data[:content_hash],
|
|
244
|
-
size_bytes: data[:content]&.bytesize || 0,
|
|
245
|
-
depositor_id: data[:depositor_id],
|
|
246
|
-
trust_notice: data[:trust_notice]
|
|
247
|
-
}
|
|
248
|
-
else
|
|
249
|
-
{ success: false, error: "HTTP #{response.code}: #{response.body}" }
|
|
250
|
-
end
|
|
251
|
-
rescue StandardError => e
|
|
252
|
-
{ success: false, error: e.message }
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
def get_skill_from_relay(url, skill_id)
|
|
256
|
-
uri = URI.parse("#{url}/place/v1/skills/content/#{URI.encode_www_form_component(skill_id)}")
|
|
257
|
-
response = Net::HTTP.get_response(uri)
|
|
258
|
-
if response.is_a?(Net::HTTPSuccess)
|
|
259
|
-
data = JSON.parse(response.body, symbolize_names: true)
|
|
260
|
-
{ success: true, skill_name: data[:name] || skill_id, format: data[:format] || 'markdown', content: data[:content], content_hash: data[:content_hash], size_bytes: data[:content]&.bytesize || 0 }
|
|
261
|
-
else
|
|
262
|
-
{ success: false, error: "HTTP #{response.code}" }
|
|
263
|
-
end
|
|
264
|
-
rescue StandardError => e
|
|
265
|
-
{ success: false, error: e.message }
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
def get_skill_direct(endpoint, skill_id, bearer_token: nil)
|
|
269
|
-
uri = URI.parse("#{endpoint}/meeting/v1/skill_content")
|
|
270
|
-
http = Net::HTTP.new(uri.host, uri.port); http.use_ssl = (uri.scheme == 'https')
|
|
271
|
-
http.open_timeout = 5; http.read_timeout = 10
|
|
272
|
-
req = Net::HTTP::Post.new(uri.path)
|
|
273
|
-
req['Content-Type'] = 'application/json'
|
|
274
|
-
req['Authorization'] = "Bearer #{bearer_token}" if bearer_token
|
|
275
|
-
req.body = JSON.generate({ skill_id: skill_id })
|
|
276
|
-
response = http.request(req)
|
|
277
|
-
if response.is_a?(Net::HTTPSuccess)
|
|
278
|
-
data = JSON.parse(response.body, symbolize_names: true)
|
|
279
|
-
payload = data.dig(:message, :payload) || data[:payload] || data
|
|
280
|
-
{ success: true, skill_name: payload[:skill_name] || skill_id, format: payload[:format] || 'markdown', content: payload[:content], content_hash: payload[:content_hash], size_bytes: payload[:content]&.bytesize || 0 }
|
|
281
|
-
else
|
|
282
|
-
{ success: false, error: response.body }
|
|
283
|
-
end
|
|
284
|
-
rescue StandardError => e
|
|
285
|
-
{ success: false, error: e.message }
|
|
286
|
-
end
|
|
287
303
|
end
|
|
288
304
|
end
|
|
289
305
|
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'net/http'
|
|
4
|
-
require 'uri'
|
|
5
3
|
require 'json'
|
|
6
4
|
|
|
7
5
|
module KairosMcp
|
|
@@ -43,30 +41,21 @@ module KairosMcp
|
|
|
43
41
|
end
|
|
44
42
|
|
|
45
43
|
def call(arguments)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return text_content(JSON.pretty_generate({ error: 'Meeting Protocol is disabled' }))
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
connection = load_connection_state
|
|
52
|
-
unless connection
|
|
53
|
-
return text_content(JSON.pretty_generate({ error: 'Not connected', hint: 'Use meeting_connect first' }))
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
url = connection['url'] || connection[:url]
|
|
57
|
-
token = connection['session_token'] || connection[:session_token]
|
|
44
|
+
client = build_place_client
|
|
45
|
+
return client if client.is_a?(Array)
|
|
58
46
|
|
|
59
47
|
begin
|
|
60
48
|
page_size = [[arguments['page_size'] || 20, 50].min, 1].max
|
|
61
|
-
params = { 'limit' => page_size.to_s }
|
|
62
|
-
params['tags'] = arguments['tags'].join(',') if arguments['tags'] && !arguments['tags'].empty?
|
|
63
|
-
params['search'] = arguments['search'] if arguments['search']
|
|
64
|
-
params['type'] = arguments['type'] if arguments['type']
|
|
65
49
|
|
|
66
|
-
result =
|
|
50
|
+
result = client.browse(
|
|
51
|
+
type: arguments['type'],
|
|
52
|
+
search: arguments['search'],
|
|
53
|
+
tags: arguments['tags'],
|
|
54
|
+
limit: page_size
|
|
55
|
+
)
|
|
67
56
|
|
|
68
|
-
|
|
69
|
-
return text_content(JSON.pretty_generate({ error: 'Failed to browse Meeting Place' }))
|
|
57
|
+
if result[:error]
|
|
58
|
+
return text_content(JSON.pretty_generate({ error: 'Failed to browse Meeting Place', message: result[:error] }))
|
|
70
59
|
end
|
|
71
60
|
|
|
72
61
|
entries = result[:entries] || []
|
|
@@ -79,7 +68,6 @@ module KairosMcp
|
|
|
79
68
|
hint: entries.empty? ? 'No skills found. Try different filters or wait for agents to deposit skills.' : 'Use meeting_acquire_skill(skill_id: "...") to acquire a skill.'
|
|
80
69
|
}
|
|
81
70
|
|
|
82
|
-
# Pass through place-level trust info (factual metadata only)
|
|
83
71
|
output[:place_trust] = result[:place_trust] if result[:place_trust]
|
|
84
72
|
|
|
85
73
|
# Check for pending attestation nudge
|
|
@@ -100,25 +88,31 @@ module KairosMcp
|
|
|
100
88
|
|
|
101
89
|
private
|
|
102
90
|
|
|
91
|
+
def build_place_client
|
|
92
|
+
config = ::MMP.load_config
|
|
93
|
+
unless config['enabled']
|
|
94
|
+
return text_content(JSON.pretty_generate({ error: 'Meeting Protocol is disabled' }))
|
|
95
|
+
end
|
|
96
|
+
connection = load_connection_state
|
|
97
|
+
unless connection
|
|
98
|
+
return text_content(JSON.pretty_generate({ error: 'Not connected', hint: 'Use meeting_connect first' }))
|
|
99
|
+
end
|
|
100
|
+
url = connection['url'] || connection[:url]
|
|
101
|
+
token = connection['session_token'] || connection[:session_token]
|
|
102
|
+
agent_id = connection['agent_id'] || connection[:agent_id]
|
|
103
|
+
identity = ::MMP::Identity.new(config: config)
|
|
104
|
+
::MMP::PlaceClient.reconnect(
|
|
105
|
+
place_url: url, identity: identity,
|
|
106
|
+
session_token: token, agent_id: agent_id
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
103
110
|
def load_connection_state
|
|
104
111
|
f = File.join(KairosMcp.storage_dir, 'meeting_connection.json')
|
|
105
112
|
File.exist?(f) ? JSON.parse(File.read(f)) : nil
|
|
106
113
|
rescue StandardError; nil
|
|
107
114
|
end
|
|
108
115
|
|
|
109
|
-
def browse_place(url, token, params)
|
|
110
|
-
query = URI.encode_www_form(params)
|
|
111
|
-
uri = URI.parse("#{url}/place/v1/board/browse?#{query}")
|
|
112
|
-
http = Net::HTTP.new(uri.host, uri.port); http.use_ssl = (uri.scheme == 'https')
|
|
113
|
-
http.open_timeout = 5; http.read_timeout = 10
|
|
114
|
-
req = Net::HTTP::Get.new(uri)
|
|
115
|
-
req['Authorization'] = "Bearer #{token}" if token
|
|
116
|
-
response = http.request(req)
|
|
117
|
-
response.is_a?(Net::HTTPSuccess) ? JSON.parse(response.body, symbolize_names: true) : nil
|
|
118
|
-
rescue StandardError
|
|
119
|
-
nil
|
|
120
|
-
end
|
|
121
|
-
|
|
122
116
|
def format_entry(entry)
|
|
123
117
|
base = {
|
|
124
118
|
type: entry[:type] || entry[:format],
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'net/http'
|
|
4
|
-
require 'uri'
|
|
5
3
|
require 'json'
|
|
6
4
|
require 'yaml'
|
|
7
5
|
require 'digest'
|
|
@@ -42,20 +40,11 @@ module KairosMcp
|
|
|
42
40
|
end
|
|
43
41
|
|
|
44
42
|
def call(arguments)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return text_content(JSON.pretty_generate({ error: 'Meeting Protocol is disabled' }))
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
connection = load_connection_state
|
|
51
|
-
unless connection
|
|
52
|
-
return text_content(JSON.pretty_generate({ error: 'Not connected', hint: 'Use meeting_connect first' }))
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
url = connection['url'] || connection[:url]
|
|
56
|
-
token = connection['session_token'] || connection[:session_token]
|
|
43
|
+
client = build_place_client(timeout: 15)
|
|
44
|
+
return client if client.is_a?(Array)
|
|
57
45
|
|
|
58
46
|
begin
|
|
47
|
+
config = ::MMP.load_config
|
|
59
48
|
identity = ::MMP::Identity.new(config: config)
|
|
60
49
|
crypto = identity.crypto
|
|
61
50
|
|
|
@@ -79,7 +68,7 @@ module KairosMcp
|
|
|
79
68
|
content_hash = Digest::SHA256.hexdigest(content)
|
|
80
69
|
signature = crypto.has_keypair? ? crypto.sign(content) : nil
|
|
81
70
|
|
|
82
|
-
result =
|
|
71
|
+
result = client.deposit({
|
|
83
72
|
skill_id: skill[:name],
|
|
84
73
|
name: skill[:name],
|
|
85
74
|
description: skill[:description],
|
|
@@ -117,6 +106,25 @@ module KairosMcp
|
|
|
117
106
|
|
|
118
107
|
private
|
|
119
108
|
|
|
109
|
+
def build_place_client(timeout: 10)
|
|
110
|
+
config = ::MMP.load_config
|
|
111
|
+
unless config['enabled']
|
|
112
|
+
return text_content(JSON.pretty_generate({ error: 'Meeting Protocol is disabled' }))
|
|
113
|
+
end
|
|
114
|
+
connection = load_connection_state
|
|
115
|
+
unless connection
|
|
116
|
+
return text_content(JSON.pretty_generate({ error: 'Not connected', hint: 'Use meeting_connect first' }))
|
|
117
|
+
end
|
|
118
|
+
url = connection['url'] || connection[:url]
|
|
119
|
+
token = connection['session_token'] || connection[:session_token]
|
|
120
|
+
agent_id = connection['agent_id'] || connection[:agent_id]
|
|
121
|
+
identity = ::MMP::Identity.new(config: config)
|
|
122
|
+
::MMP::PlaceClient.reconnect(
|
|
123
|
+
place_url: url, identity: identity,
|
|
124
|
+
session_token: token, agent_id: agent_id, timeout: timeout
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
120
128
|
def load_connection_state
|
|
121
129
|
f = File.join(KairosMcp.storage_dir, 'meeting_connection.json')
|
|
122
130
|
File.exist?(f) ? JSON.parse(File.read(f)) : nil
|
|
@@ -170,20 +178,6 @@ module KairosMcp
|
|
|
170
178
|
rescue StandardError
|
|
171
179
|
0
|
|
172
180
|
end
|
|
173
|
-
|
|
174
|
-
def deposit_to_place(url, token, skill)
|
|
175
|
-
uri = URI.parse("#{url}/place/v1/deposit")
|
|
176
|
-
http = Net::HTTP.new(uri.host, uri.port); http.use_ssl = (uri.scheme == 'https')
|
|
177
|
-
http.open_timeout = 5; http.read_timeout = 15
|
|
178
|
-
req = Net::HTTP::Post.new(uri.path)
|
|
179
|
-
req['Content-Type'] = 'application/json'
|
|
180
|
-
req['Authorization'] = "Bearer #{token}" if token
|
|
181
|
-
req.body = JSON.generate(skill)
|
|
182
|
-
response = http.request(req)
|
|
183
|
-
JSON.parse(response.body, symbolize_names: true)
|
|
184
|
-
rescue StandardError => e
|
|
185
|
-
{ error: e.message }
|
|
186
|
-
end
|
|
187
181
|
end
|
|
188
182
|
end
|
|
189
183
|
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'net/http'
|
|
4
|
-
require 'uri'
|
|
5
3
|
require 'json'
|
|
6
4
|
require 'digest'
|
|
7
5
|
|
|
@@ -64,13 +62,29 @@ module KairosMcp
|
|
|
64
62
|
filter_tags = arguments['tags']
|
|
65
63
|
|
|
66
64
|
begin
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
source_client = build_client_for(url: source_url, token: source_token)
|
|
66
|
+
target_client = build_client_for(url: target_url, token: target_token, timeout: 15)
|
|
67
|
+
|
|
68
|
+
# Resolve Place ID for provenance chain
|
|
69
|
+
info_result = source_client.place_info
|
|
70
|
+
source_place_id = info_result[:place_id] || source_url
|
|
69
71
|
|
|
70
72
|
# Step 1: Browse source Place for deposited skills
|
|
71
|
-
|
|
72
|
-
if
|
|
73
|
-
return text_content(JSON.pretty_generate({ error: 'Failed to browse source Place' }))
|
|
73
|
+
source_result = source_client.browse(type: 'deposited_skill', tags: filter_tags, limit: 50)
|
|
74
|
+
if source_result[:error]
|
|
75
|
+
return text_content(JSON.pretty_generate({ error: 'Failed to browse source Place', message: source_result[:error] }))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
source_skills = (source_result[:entries] || []).map do |e|
|
|
79
|
+
{
|
|
80
|
+
skill_id: e[:skill_id],
|
|
81
|
+
name: e[:name],
|
|
82
|
+
description: e[:description],
|
|
83
|
+
tags: e[:tags],
|
|
84
|
+
owner_agent_id: e[:agent_id],
|
|
85
|
+
deposited_at: e[:deposited_at],
|
|
86
|
+
trust_metadata: e[:trust_metadata]
|
|
87
|
+
}
|
|
74
88
|
end
|
|
75
89
|
|
|
76
90
|
# Filter by skill_ids if specified
|
|
@@ -91,11 +105,11 @@ module KairosMcp
|
|
|
91
105
|
|
|
92
106
|
source_skills.each do |skill_meta|
|
|
93
107
|
# GET full content from source
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
skill_meta[:
|
|
97
|
-
owner_agent_id: skill_meta[:owner_agent_id]
|
|
108
|
+
raw = source_client.get_skill_content(
|
|
109
|
+
skill_id: skill_meta[:skill_id],
|
|
110
|
+
owner: skill_meta[:owner_agent_id]
|
|
98
111
|
)
|
|
112
|
+
content_result = adapt_place_skill_content(raw, skill_meta[:skill_id])
|
|
99
113
|
|
|
100
114
|
unless content_result[:success]
|
|
101
115
|
failed << { skill_id: skill_meta[:skill_id], error: content_result[:error] }
|
|
@@ -115,7 +129,7 @@ module KairosMcp
|
|
|
115
129
|
signature = crypto.has_keypair? ? crypto.sign(content) : nil
|
|
116
130
|
|
|
117
131
|
# POST to target Place with provenance
|
|
118
|
-
deposit_result =
|
|
132
|
+
deposit_result = target_client.deposit({
|
|
119
133
|
skill_id: skill_meta[:skill_id],
|
|
120
134
|
name: content_result[:name] || skill_meta[:name],
|
|
121
135
|
description: skill_meta[:description],
|
|
@@ -163,83 +177,37 @@ module KairosMcp
|
|
|
163
177
|
|
|
164
178
|
private
|
|
165
179
|
|
|
166
|
-
#
|
|
167
|
-
def
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
http = Net::HTTP.new(uri.host, uri.port); http.use_ssl = (uri.scheme == 'https')
|
|
174
|
-
http.open_timeout = 5; http.read_timeout = 10
|
|
175
|
-
req = Net::HTTP::Get.new(uri)
|
|
176
|
-
req['Authorization'] = "Bearer #{token}"
|
|
177
|
-
response = http.request(req)
|
|
178
|
-
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
179
|
-
|
|
180
|
-
data = JSON.parse(response.body, symbolize_names: true)
|
|
181
|
-
(data[:entries] || []).map do |e|
|
|
182
|
-
{
|
|
183
|
-
skill_id: e[:skill_id],
|
|
184
|
-
name: e[:name],
|
|
185
|
-
description: e[:description],
|
|
186
|
-
tags: e[:tags],
|
|
187
|
-
owner_agent_id: e[:agent_id],
|
|
188
|
-
deposited_at: e[:deposited_at],
|
|
189
|
-
trust_metadata: e[:trust_metadata]
|
|
190
|
-
}
|
|
191
|
-
end
|
|
192
|
-
rescue StandardError
|
|
193
|
-
nil
|
|
180
|
+
# Build PlaceClient for explicit URL/token (federate uses explicit args, not connection state)
|
|
181
|
+
def build_client_for(url:, token:, timeout: 10)
|
|
182
|
+
identity = ::MMP::Identity.new(config: ::MMP.load_config)
|
|
183
|
+
::MMP::PlaceClient.reconnect(
|
|
184
|
+
place_url: url, identity: identity,
|
|
185
|
+
session_token: token, timeout: timeout
|
|
186
|
+
)
|
|
194
187
|
end
|
|
195
188
|
|
|
196
|
-
#
|
|
197
|
-
def
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
http = Net::HTTP.new(uri.host, uri.port); http.use_ssl = (uri.scheme == 'https')
|
|
202
|
-
http.open_timeout = 5; http.read_timeout = 10
|
|
203
|
-
req = Net::HTTP::Get.new(uri)
|
|
204
|
-
req['Authorization'] = "Bearer #{token}"
|
|
205
|
-
response = http.request(req)
|
|
206
|
-
if response.is_a?(Net::HTTPSuccess)
|
|
207
|
-
data = JSON.parse(response.body, symbolize_names: true)
|
|
189
|
+
# Adapter A1: Place deposit skill content
|
|
190
|
+
def adapt_place_skill_content(raw, skill_id)
|
|
191
|
+
if raw[:error]
|
|
192
|
+
{ success: false, error: raw[:error] }
|
|
193
|
+
else
|
|
208
194
|
{
|
|
209
195
|
success: true,
|
|
210
|
-
name:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
196
|
+
name: raw[:name] || skill_id,
|
|
197
|
+
skill_name: raw[:name] || skill_id,
|
|
198
|
+
format: raw[:format] || 'markdown',
|
|
199
|
+
content: raw[:content],
|
|
200
|
+
content_hash: raw[:content_hash],
|
|
201
|
+
size_bytes: raw[:content]&.bytesize || 0,
|
|
202
|
+
depositor_id: raw[:depositor_id],
|
|
203
|
+
trust_notice: raw[:trust_notice]
|
|
215
204
|
}
|
|
216
|
-
else
|
|
217
|
-
{ success: false, error: "HTTP #{response.code}" }
|
|
218
205
|
end
|
|
219
|
-
rescue StandardError => e
|
|
220
|
-
{ success: false, error: e.message }
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
# Resolve Place's instance ID from /place/v1/info (unauthenticated).
|
|
224
|
-
# Returns nil on failure (caller should fallback to URL).
|
|
225
|
-
def resolve_place_id(url)
|
|
226
|
-
uri = URI.parse("#{url}/place/v1/info")
|
|
227
|
-
http = Net::HTTP.new(uri.host, uri.port); http.use_ssl = (uri.scheme == 'https')
|
|
228
|
-
http.open_timeout = 3; http.read_timeout = 5
|
|
229
|
-
req = Net::HTTP::Get.new(uri)
|
|
230
|
-
response = http.request(req)
|
|
231
|
-
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
232
|
-
data = JSON.parse(response.body, symbolize_names: true)
|
|
233
|
-
data[:place_id]
|
|
234
|
-
rescue StandardError
|
|
235
|
-
nil
|
|
236
206
|
end
|
|
237
207
|
|
|
238
208
|
# Build provenance for the federated deposit.
|
|
239
|
-
# source_place_id: resolved Place instance ID (or URL as fallback).
|
|
240
209
|
def build_federation_provenance(source_provenance, source_place_id, content_result, source_deposited_at)
|
|
241
210
|
if source_provenance[:hop_count].to_i > 0
|
|
242
|
-
# Already federated: increment hop, append source to via
|
|
243
211
|
{
|
|
244
212
|
origin_place_id: source_provenance[:origin_place_id],
|
|
245
213
|
origin_agent_id: source_provenance[:origin_agent_id] || content_result[:depositor_id],
|
|
@@ -248,7 +216,6 @@ module KairosMcp
|
|
|
248
216
|
deposited_at_origin: source_provenance[:deposited_at_origin]
|
|
249
217
|
}
|
|
250
218
|
else
|
|
251
|
-
# First federation: source Place is the origin
|
|
252
219
|
{
|
|
253
220
|
origin_place_id: source_place_id,
|
|
254
221
|
origin_agent_id: content_result[:depositor_id],
|
|
@@ -258,21 +225,6 @@ module KairosMcp
|
|
|
258
225
|
}
|
|
259
226
|
end
|
|
260
227
|
end
|
|
261
|
-
|
|
262
|
-
# POST skill to target Place with provenance
|
|
263
|
-
def deposit_to_target(url, token, skill)
|
|
264
|
-
uri = URI.parse("#{url}/place/v1/deposit")
|
|
265
|
-
http = Net::HTTP.new(uri.host, uri.port); http.use_ssl = (uri.scheme == 'https')
|
|
266
|
-
http.open_timeout = 5; http.read_timeout = 15
|
|
267
|
-
req = Net::HTTP::Post.new(uri.path)
|
|
268
|
-
req['Content-Type'] = 'application/json'
|
|
269
|
-
req['Authorization'] = "Bearer #{token}"
|
|
270
|
-
req.body = JSON.generate(skill)
|
|
271
|
-
response = http.request(req)
|
|
272
|
-
JSON.parse(response.body, symbolize_names: true)
|
|
273
|
-
rescue StandardError => e
|
|
274
|
-
{ error: e.message }
|
|
275
|
-
end
|
|
276
228
|
end
|
|
277
229
|
end
|
|
278
230
|
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'net/http'
|
|
4
|
-
require 'uri'
|
|
5
3
|
require 'json'
|
|
6
4
|
|
|
7
5
|
module KairosMcp
|
|
@@ -34,8 +32,7 @@ module KairosMcp
|
|
|
34
32
|
type: 'object',
|
|
35
33
|
properties: {
|
|
36
34
|
peer_id: { type: 'string', description: 'ID of the peer agent' },
|
|
37
|
-
skill_id: { type: 'string', description: 'ID of the skill' }
|
|
38
|
-
include_preview: { type: 'boolean', description: 'Include content preview (default: false)' }
|
|
35
|
+
skill_id: { type: 'string', description: 'ID of the skill' }
|
|
39
36
|
},
|
|
40
37
|
required: %w[peer_id skill_id]
|
|
41
38
|
}
|
|
@@ -44,12 +41,6 @@ module KairosMcp
|
|
|
44
41
|
def call(arguments)
|
|
45
42
|
peer_id = arguments['peer_id']
|
|
46
43
|
skill_id = arguments['skill_id']
|
|
47
|
-
include_preview = arguments['include_preview'] || false
|
|
48
|
-
|
|
49
|
-
config = ::MMP.load_config
|
|
50
|
-
unless config['enabled']
|
|
51
|
-
return text_content(JSON.pretty_generate({ error: 'Meeting Protocol is disabled' }))
|
|
52
|
-
end
|
|
53
44
|
|
|
54
45
|
connection = load_connection_state
|
|
55
46
|
unless connection
|
|
@@ -62,31 +53,36 @@ module KairosMcp
|
|
|
62
53
|
end
|
|
63
54
|
|
|
64
55
|
begin
|
|
65
|
-
|
|
66
|
-
url = connection['url'] || connection[:url]
|
|
56
|
+
# Target: peer endpoint if available, otherwise Place URL
|
|
67
57
|
endpoint = peer['endpoint'] || peer[:endpoint]
|
|
68
|
-
|
|
69
|
-
# In relay mode, use the peer's endpoint if available,
|
|
70
|
-
# otherwise fall back to the connection URL (Meeting Place itself)
|
|
58
|
+
url = connection['url'] || connection[:url]
|
|
71
59
|
target = endpoint || url
|
|
72
|
-
token = connection['session_token'] || connection[:session_token]
|
|
73
|
-
details = get_details_direct(target, skill_id, bearer_token: token)
|
|
74
60
|
|
|
75
|
-
|
|
61
|
+
client = build_meeting_client(url_override: target, timeout: 5)
|
|
62
|
+
return client if client.is_a?(Array)
|
|
63
|
+
|
|
64
|
+
raw = client.get_skill_details(skill_id: skill_id)
|
|
65
|
+
details = adapt_skill_details(raw)
|
|
66
|
+
|
|
67
|
+
if details.nil?
|
|
76
68
|
return text_content(JSON.pretty_generate({ error: "Skill not found: #{skill_id}" }))
|
|
77
69
|
end
|
|
78
70
|
|
|
71
|
+
if details[:error]
|
|
72
|
+
return text_content(JSON.pretty_generate({ error: details[:error], message: details[:message] }))
|
|
73
|
+
end
|
|
74
|
+
|
|
79
75
|
result = {
|
|
80
76
|
peer_id: peer_id,
|
|
81
77
|
peer_name: peer['name'] || peer[:name],
|
|
82
78
|
skill: {
|
|
83
79
|
id: skill_id,
|
|
84
|
-
name: details[
|
|
85
|
-
description: details[
|
|
86
|
-
version: details[
|
|
87
|
-
format: details[
|
|
88
|
-
tags: details[
|
|
89
|
-
size_bytes: details[
|
|
80
|
+
name: details[:name],
|
|
81
|
+
description: details[:description],
|
|
82
|
+
version: details[:version],
|
|
83
|
+
format: details[:format],
|
|
84
|
+
tags: details[:tags],
|
|
85
|
+
size_bytes: details[:size_bytes]
|
|
90
86
|
},
|
|
91
87
|
hint: "To acquire: meeting_acquire_skill(peer_id: \"#{peer_id}\", skill_id: \"#{skill_id}\")"
|
|
92
88
|
}
|
|
@@ -119,22 +115,52 @@ module KairosMcp
|
|
|
119
115
|
(connection['peers'] || connection[:peers] || []).find { |p| (p['agent_id'] || p[:agent_id]) == peer_id }
|
|
120
116
|
end
|
|
121
117
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
118
|
+
# Build PlaceClient with meeting_session_token for /meeting/v1/* endpoints
|
|
119
|
+
def build_meeting_client(url_override: nil, timeout: 10)
|
|
120
|
+
config = ::MMP.load_config
|
|
121
|
+
unless config['enabled']
|
|
122
|
+
return text_content(JSON.pretty_generate({ error: 'Meeting Protocol is disabled' }))
|
|
123
|
+
end
|
|
124
|
+
connection = load_connection_state
|
|
125
|
+
unless connection
|
|
126
|
+
return text_content(JSON.pretty_generate({ error: 'Not connected', hint: 'Use meeting_connect first' }))
|
|
127
|
+
end
|
|
128
|
+
url = url_override || connection['url'] || connection[:url]
|
|
129
|
+
token = connection['meeting_session_token'] || connection[:meeting_session_token]
|
|
130
|
+
unless token
|
|
131
|
+
return text_content(JSON.pretty_generate({
|
|
132
|
+
error: 'No meeting session token',
|
|
133
|
+
hint: 'meeting_connect may have failed the /meeting/v1/introduce handshake. Reconnect.'
|
|
134
|
+
}))
|
|
135
|
+
end
|
|
136
|
+
identity = ::MMP::Identity.new(config: config)
|
|
137
|
+
::MMP::PlaceClient.reconnect(
|
|
138
|
+
place_url: url, identity: identity,
|
|
139
|
+
session_token: token, timeout: timeout
|
|
140
|
+
)
|
|
127
141
|
end
|
|
128
142
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
143
|
+
# Adapter A3: skill_details response normalization
|
|
144
|
+
# Distinguishes "not found" from auth/network errors (R3-1 fix)
|
|
145
|
+
def adapt_skill_details(raw)
|
|
146
|
+
if raw[:error]
|
|
147
|
+
# Distinguish not_found from other errors
|
|
148
|
+
if raw[:error].to_s.include?('not_found') || raw[:error].to_s.include?('404')
|
|
149
|
+
nil
|
|
150
|
+
else
|
|
151
|
+
{ error: raw[:error], message: raw[:message] }
|
|
152
|
+
end
|
|
153
|
+
else
|
|
154
|
+
meta = raw[:metadata] || raw
|
|
155
|
+
{
|
|
156
|
+
name: meta[:name],
|
|
157
|
+
description: meta[:description] || meta[:summary],
|
|
158
|
+
version: meta[:version] || '1.0',
|
|
159
|
+
format: meta[:format] || 'markdown',
|
|
160
|
+
tags: meta[:tags] || [],
|
|
161
|
+
size_bytes: meta[:size_bytes]
|
|
162
|
+
}
|
|
163
|
+
end
|
|
138
164
|
end
|
|
139
165
|
end
|
|
140
166
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kairos-chain
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Masaomi Hatakeyama
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: minitest
|