poetry-agent 0.0.2
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 +7 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +51 -0
- data/app/javascript/poetry/agent/a2ui_surface_controller.js +141 -0
- data/app/javascript/poetry/agent/adapter.js +77 -0
- data/app/javascript/poetry/agent/agui_client_tool_controller.js +53 -0
- data/app/javascript/poetry/agent/index.js +41 -0
- data/app/javascript/poetry/agent/stream_actions.js +65 -0
- data/app/javascript/poetry/agent/webmcp_controller.js +248 -0
- data/app/javascript/poetry/agent/webmcp_form_controller.js +109 -0
- data/config/controllers_manifest.json +82 -0
- data/config/importmap.rb +10 -0
- data/exe/poetry-agent +28 -0
- data/lib/poetry/agent/a2ui/catalog.rb +289 -0
- data/lib/poetry/agent/a2ui/catalogs/basic.rb +460 -0
- data/lib/poetry/agent/a2ui/catalogs/native.rb +176 -0
- data/lib/poetry/agent/a2ui/checks.rb +45 -0
- data/lib/poetry/agent/a2ui/evaluator.rb +139 -0
- data/lib/poetry/agent/a2ui/expression.rb +175 -0
- data/lib/poetry/agent/a2ui/functions.rb +417 -0
- data/lib/poetry/agent/a2ui/markdown.rb +63 -0
- data/lib/poetry/agent/a2ui/pointer.rb +113 -0
- data/lib/poetry/agent/a2ui/protocol.rb +12 -0
- data/lib/poetry/agent/a2ui/renderer.rb +242 -0
- data/lib/poetry/agent/a2ui/session.rb +302 -0
- data/lib/poetry/agent/a2ui/streams.rb +82 -0
- data/lib/poetry/agent/a2ui/surface.rb +352 -0
- data/lib/poetry/agent/a2ui.rb +48 -0
- data/lib/poetry/agent/agui/client.rb +69 -0
- data/lib/poetry/agent/agui/json_patch.rb +137 -0
- data/lib/poetry/agent/agui/relay.rb +105 -0
- data/lib/poetry/agent/agui/run_input.rb +83 -0
- data/lib/poetry/agent/agui/sse.rb +97 -0
- data/lib/poetry/agent/agui/transcript.rb +540 -0
- data/lib/poetry/agent/agui/turbo_stream.rb +68 -0
- data/lib/poetry/agent/agui.rb +87 -0
- data/lib/poetry/agent/config.rb +49 -0
- data/lib/poetry/agent/engine.rb +37 -0
- data/lib/poetry/agent/mcp/bundled.rb +54 -0
- data/lib/poetry/agent/mcp/http.rb +89 -0
- data/lib/poetry/agent/mcp/server.rb +962 -0
- data/lib/poetry/agent/version.rb +8 -0
- data/lib/poetry/agent/webmcp/origin_trial.rb +49 -0
- data/lib/poetry/agent/webmcp.rb +37 -0
- data/lib/poetry/agent.rb +66 -0
- data/lib/poetry-agent.rb +4 -0
- metadata +117 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Agent
|
|
5
|
+
# The gem's configuration. Every WebMCP surface is OFF until a rendered
|
|
6
|
+
# instance opts in; the settings here shape delivery and budgets, never
|
|
7
|
+
# exposure.
|
|
8
|
+
class Config
|
|
9
|
+
# Origin-trial tokens the {OriginTrial} middleware serves in the
|
|
10
|
+
# `Origin-Trial` response header (one per browser trial - Chrome and
|
|
11
|
+
# Edge run separate trials). Empty by default: local development
|
|
12
|
+
# enables the API through the browser flag instead.
|
|
13
|
+
#
|
|
14
|
+
# @return [Array<String>]
|
|
15
|
+
attr_accessor :origin_trial_tokens
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@origin_trial_tokens = []
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The per-page registration budget the registrar enforces: past
|
|
22
|
+
# this many registered tools on one document, further registrations
|
|
23
|
+
# are dropped with a console warning (each tool costs the agent
|
|
24
|
+
# context; overlap confuses tool choice). Stored on poetry-core's
|
|
25
|
+
# configuration, where the component contract reads it to put the
|
|
26
|
+
# budget on every opted-in root - this accessor writes through.
|
|
27
|
+
#
|
|
28
|
+
# @return [Integer]
|
|
29
|
+
def registration_budget
|
|
30
|
+
Poetry::Core::Config.current.webmcp_registration_budget
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Sets the per-page registration budget.
|
|
34
|
+
#
|
|
35
|
+
# @param count [Integer]
|
|
36
|
+
# @return [Integer]
|
|
37
|
+
def registration_budget=(count)
|
|
38
|
+
Poetry::Core::Config.current.webmcp_registration_budget = Integer(count)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The process-wide configuration instance.
|
|
42
|
+
#
|
|
43
|
+
# @return [Config]
|
|
44
|
+
def self.current
|
|
45
|
+
@current ||= new
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/engine"
|
|
4
|
+
require "action_dispatch"
|
|
5
|
+
|
|
6
|
+
module Poetry
|
|
7
|
+
module Agent
|
|
8
|
+
# The Rails engine: merges the WebMCP controllers manifest into
|
|
9
|
+
# poetry-core's catalog (so `webmcp:` roots validate at render), serves
|
|
10
|
+
# the runtime JavaScript through the importmap-first channel, and
|
|
11
|
+
# mounts the origin-trial middleware. Loading the gem is the only
|
|
12
|
+
# integration step; the host imports `@poetry/agent` beside
|
|
13
|
+
# `@poetry/controllers`.
|
|
14
|
+
class Engine < ::Rails::Engine
|
|
15
|
+
initializer "poetry_agent.controllers_manifest", before: :eager_load! do
|
|
16
|
+
Poetry::Core::Stimulus::Manifest.register(Poetry::Agent::WebMCP.manifest_path.to_s)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
initializer "poetry_agent.origin_trial" do |app|
|
|
20
|
+
app.config.middleware.use Poetry::Agent::WebMCP::OriginTrial
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
initializer "poetry_agent.assets" do |app|
|
|
24
|
+
app.config.assets.paths << Poetry::Agent.root.join("app/javascript").to_s if app.config.respond_to?(:assets)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The importmap-first JS channel (the poetry-core shape); bundler
|
|
28
|
+
# hosts use the @poetry/agent npm package instead.
|
|
29
|
+
initializer "poetry_agent.importmap", before: "importmap" do |app|
|
|
30
|
+
if app.config.respond_to?(:importmap)
|
|
31
|
+
app.config.importmap.paths << Poetry::Agent.root.join("config/importmap.rb")
|
|
32
|
+
app.config.importmap.cache_sweepers << Poetry::Agent.root.join("app/javascript")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Agent
|
|
5
|
+
module MCP
|
|
6
|
+
# The server as the exe assembles it: the registry root defaults to
|
|
7
|
+
# the bundled poetry-ui gem, poetry-lucide's icon names power check's
|
|
8
|
+
# icon-membership tier, and poetry-ui's skills, helper names, and
|
|
9
|
+
# recipes ride along when the gem is bundled - each a soft require,
|
|
10
|
+
# so the same assembly serves a core-only host. ONE assembly for the
|
|
11
|
+
# exe and the HTTP mount, so no surface can lag another.
|
|
12
|
+
module Bundled
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# @param root [String, nil] a registry root; defaults to poetry-ui
|
|
16
|
+
# @param app_root [String] the host app (build_page's probe/direct
|
|
17
|
+
# steps read its config/theme)
|
|
18
|
+
# @return [Server]
|
|
19
|
+
# @raise [ArgumentError] when no component registry is found
|
|
20
|
+
def server(root: nil, app_root: Dir.pwd)
|
|
21
|
+
ui = soft_require("poetry/ui")
|
|
22
|
+
root ||= ui ? Poetry::Ui.root.to_s : Gem::Specification.find_all_by_name("poetry-ui").first&.gem_dir
|
|
23
|
+
registry = root && File.join(root, Poetry::Core::Registry::RELATIVE_PATH)
|
|
24
|
+
unless registry && File.exist?(registry)
|
|
25
|
+
raise ArgumentError, "no component registry at #{registry || "(no poetry-ui found)"} - pass root:"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
skills, helpers, recipes =
|
|
29
|
+
ui ? [Poetry::Ui.agent_skills, Poetry::Ui.helper_names, Poetry::Ui.recipe_items.summaries] : [{}, nil, []]
|
|
30
|
+
Server.from_registry(root, icon_names: icon_names, helpers: helpers, skills: skills,
|
|
31
|
+
app_root: app_root, recipes: recipes)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The lucide names, or nil for a host without poetry-lucide (check
|
|
35
|
+
# then validates icon-name shape only).
|
|
36
|
+
def icon_names
|
|
37
|
+
return nil unless soft_require("poetry/lucide")
|
|
38
|
+
|
|
39
|
+
Poetry::Core::Icons.set(:lucide).names
|
|
40
|
+
rescue Poetry::Core::Error
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @api private
|
|
45
|
+
def soft_require(feature)
|
|
46
|
+
require feature
|
|
47
|
+
true
|
|
48
|
+
rescue LoadError
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Poetry
|
|
6
|
+
module Agent
|
|
7
|
+
module MCP
|
|
8
|
+
# The MCP server over HTTP: a Rack app answering JSON-RPC 2.0 POSTs
|
|
9
|
+
# (the Streamable HTTP transport's request/response half) with the
|
|
10
|
+
# same pure {Server#handle} the stdio exe uses - one surface, two
|
|
11
|
+
# transports. Mount it at the conventional same-origin `/mcp`, which
|
|
12
|
+
# is where in-page bridges (Cloudflare's WebMCP Site MCP Server pack
|
|
13
|
+
# among them) look for a site's own MCP server.
|
|
14
|
+
#
|
|
15
|
+
# Read-only by construction (every tool describes or verifies), so
|
|
16
|
+
# exposing it is a discovery surface, not a mutation surface. The
|
|
17
|
+
# Origin header is validated against the request's own host, so a
|
|
18
|
+
# cross-site page cannot drive it through a visitor's browser.
|
|
19
|
+
#
|
|
20
|
+
# @example config/routes.rb
|
|
21
|
+
# mount Poetry::Agent::MCP::HTTP.new => "/mcp"
|
|
22
|
+
class HTTP
|
|
23
|
+
# The response media type.
|
|
24
|
+
JSON_TYPE = "application/json"
|
|
25
|
+
|
|
26
|
+
# @param server [Server, Proc, nil] the server, a lambda building it
|
|
27
|
+
# on first request, or nil for the bundled assembly
|
|
28
|
+
def initialize(server = nil)
|
|
29
|
+
@server = server
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The Rack entry point.
|
|
33
|
+
#
|
|
34
|
+
# @param env [Hash] the Rack environment
|
|
35
|
+
# @return [Array(Integer, Hash, Array<String>)] status, headers, body
|
|
36
|
+
def call(env)
|
|
37
|
+
request = Rack::Request.new(env)
|
|
38
|
+
return not_allowed unless request.post?
|
|
39
|
+
return forbidden unless same_origin?(request)
|
|
40
|
+
|
|
41
|
+
payload = JSON.parse(request.body.read)
|
|
42
|
+
# A batch is an Array; a single request is a Hash (never Array() a
|
|
43
|
+
# Hash - it splits into pairs).
|
|
44
|
+
messages = payload.is_a?(Array) ? payload : [payload]
|
|
45
|
+
responses = messages.filter_map { |message| server.handle(message) }
|
|
46
|
+
return [202, headers, []] if responses.empty?
|
|
47
|
+
|
|
48
|
+
body = payload.is_a?(Array) ? responses : responses.first
|
|
49
|
+
[200, headers, [JSON.generate(body)]]
|
|
50
|
+
rescue JSON::ParserError => e
|
|
51
|
+
[400, headers, [JSON.generate(rpc_error(-32_700, "parse error: #{e.message}"))]]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def server
|
|
57
|
+
@server = @server.call if @server.respond_to?(:call) && !@server.respond_to?(:handle)
|
|
58
|
+
@server ||= Bundled.server
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def headers
|
|
62
|
+
{ "content-type" => JSON_TYPE, "cache-control" => "no-store" }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def not_allowed
|
|
66
|
+
[405, headers.merge("allow" => "POST"),
|
|
67
|
+
[JSON.generate(rpc_error(-32_601, "POST JSON-RPC only (SSE streaming is not offered)"))]]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def forbidden
|
|
71
|
+
[403, headers, [JSON.generate(rpc_error(-32_600, "origin not allowed"))]]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# An absent Origin (same-origin fetch, curl, an MCP client) passes;
|
|
75
|
+
# a present one must match the request's own scheme + host.
|
|
76
|
+
def same_origin?(request)
|
|
77
|
+
origin = request.get_header("HTTP_ORIGIN")
|
|
78
|
+
return true if origin.nil? || origin.empty?
|
|
79
|
+
|
|
80
|
+
origin.casecmp?("#{request.scheme}://#{request.host_with_port}")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def rpc_error(code, message)
|
|
84
|
+
{ "jsonrpc" => "2.0", "id" => nil, "error" => { "code" => code, "message" => message } }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|