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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +51 -0
  5. data/app/javascript/poetry/agent/a2ui_surface_controller.js +141 -0
  6. data/app/javascript/poetry/agent/adapter.js +77 -0
  7. data/app/javascript/poetry/agent/agui_client_tool_controller.js +53 -0
  8. data/app/javascript/poetry/agent/index.js +41 -0
  9. data/app/javascript/poetry/agent/stream_actions.js +65 -0
  10. data/app/javascript/poetry/agent/webmcp_controller.js +248 -0
  11. data/app/javascript/poetry/agent/webmcp_form_controller.js +109 -0
  12. data/config/controllers_manifest.json +82 -0
  13. data/config/importmap.rb +10 -0
  14. data/exe/poetry-agent +28 -0
  15. data/lib/poetry/agent/a2ui/catalog.rb +289 -0
  16. data/lib/poetry/agent/a2ui/catalogs/basic.rb +460 -0
  17. data/lib/poetry/agent/a2ui/catalogs/native.rb +176 -0
  18. data/lib/poetry/agent/a2ui/checks.rb +45 -0
  19. data/lib/poetry/agent/a2ui/evaluator.rb +139 -0
  20. data/lib/poetry/agent/a2ui/expression.rb +175 -0
  21. data/lib/poetry/agent/a2ui/functions.rb +417 -0
  22. data/lib/poetry/agent/a2ui/markdown.rb +63 -0
  23. data/lib/poetry/agent/a2ui/pointer.rb +113 -0
  24. data/lib/poetry/agent/a2ui/protocol.rb +12 -0
  25. data/lib/poetry/agent/a2ui/renderer.rb +242 -0
  26. data/lib/poetry/agent/a2ui/session.rb +302 -0
  27. data/lib/poetry/agent/a2ui/streams.rb +82 -0
  28. data/lib/poetry/agent/a2ui/surface.rb +352 -0
  29. data/lib/poetry/agent/a2ui.rb +48 -0
  30. data/lib/poetry/agent/agui/client.rb +69 -0
  31. data/lib/poetry/agent/agui/json_patch.rb +137 -0
  32. data/lib/poetry/agent/agui/relay.rb +105 -0
  33. data/lib/poetry/agent/agui/run_input.rb +83 -0
  34. data/lib/poetry/agent/agui/sse.rb +97 -0
  35. data/lib/poetry/agent/agui/transcript.rb +540 -0
  36. data/lib/poetry/agent/agui/turbo_stream.rb +68 -0
  37. data/lib/poetry/agent/agui.rb +87 -0
  38. data/lib/poetry/agent/config.rb +49 -0
  39. data/lib/poetry/agent/engine.rb +37 -0
  40. data/lib/poetry/agent/mcp/bundled.rb +54 -0
  41. data/lib/poetry/agent/mcp/http.rb +89 -0
  42. data/lib/poetry/agent/mcp/server.rb +962 -0
  43. data/lib/poetry/agent/version.rb +8 -0
  44. data/lib/poetry/agent/webmcp/origin_trial.rb +49 -0
  45. data/lib/poetry/agent/webmcp.rb +37 -0
  46. data/lib/poetry/agent.rb +66 -0
  47. data/lib/poetry-agent.rb +4 -0
  48. 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