mistri 0.5.0 → 0.6.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.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +480 -4
  3. data/CONTRIBUTING.md +52 -0
  4. data/README.md +289 -385
  5. data/SECURITY.md +40 -0
  6. data/UPGRADING.md +640 -0
  7. data/assets/logo-animated.svg +30 -0
  8. data/assets/logo-dark.svg +14 -0
  9. data/assets/logo-light.svg +14 -0
  10. data/assets/logo.svg +14 -0
  11. data/assets/social-preview.png +0 -0
  12. data/docs/README.md +87 -0
  13. data/docs/context-and-workspaces.md +378 -0
  14. data/docs/mcp.md +366 -0
  15. data/docs/reliability.md +450 -0
  16. data/docs/sessions.md +295 -0
  17. data/docs/sub-agents.md +401 -0
  18. data/docs/tool-contracts.md +324 -0
  19. data/examples/approval.rb +36 -0
  20. data/examples/browser.rb +27 -0
  21. data/examples/page_editor.rb +31 -0
  22. data/examples/quickstart.rb +21 -0
  23. data/lib/generators/mistri/install/install_generator.rb +7 -3
  24. data/lib/generators/mistri/install/templates/migration.rb.tt +2 -2
  25. data/lib/generators/mistri/mcp/templates/migration.rb.tt +1 -1
  26. data/lib/generators/mistri/mcp/templates/model.rb.tt +15 -8
  27. data/lib/mistri/agent.rb +575 -55
  28. data/lib/mistri/budget.rb +26 -1
  29. data/lib/mistri/child.rb +72 -16
  30. data/lib/mistri/compaction.rb +26 -10
  31. data/lib/mistri/compactor.rb +34 -11
  32. data/lib/mistri/console.rb +28 -7
  33. data/lib/mistri/content.rb +9 -3
  34. data/lib/mistri/dispatchers.rb +14 -12
  35. data/lib/mistri/errors.rb +83 -4
  36. data/lib/mistri/event.rb +24 -8
  37. data/lib/mistri/event_delivery.rb +60 -0
  38. data/lib/mistri/locks.rb +3 -3
  39. data/lib/mistri/mcp/client.rb +74 -19
  40. data/lib/mistri/mcp/egress.rb +216 -0
  41. data/lib/mistri/mcp/oauth.rb +476 -127
  42. data/lib/mistri/mcp/wires.rb +115 -23
  43. data/lib/mistri/mcp.rb +42 -8
  44. data/lib/mistri/message.rb +21 -11
  45. data/lib/mistri/models.rb +160 -22
  46. data/lib/mistri/providers/anthropic/assembler.rb +282 -44
  47. data/lib/mistri/providers/anthropic/serializer.rb +14 -9
  48. data/lib/mistri/providers/anthropic.rb +29 -6
  49. data/lib/mistri/providers/fake.rb +26 -10
  50. data/lib/mistri/providers/gemini/assembler.rb +148 -21
  51. data/lib/mistri/providers/gemini/serializer.rb +78 -9
  52. data/lib/mistri/providers/gemini.rb +31 -5
  53. data/lib/mistri/providers/openai/assembler.rb +337 -60
  54. data/lib/mistri/providers/openai/serializer.rb +13 -12
  55. data/lib/mistri/providers/openai.rb +29 -5
  56. data/lib/mistri/providers/schema_capabilities.rb +214 -0
  57. data/lib/mistri/result.rb +1 -1
  58. data/lib/mistri/schema.rb +893 -75
  59. data/lib/mistri/session.rb +560 -48
  60. data/lib/mistri/sinks/coalesced.rb +17 -10
  61. data/lib/mistri/spawner.rb +111 -61
  62. data/lib/mistri/sse.rb +57 -14
  63. data/lib/mistri/stores/active_record.rb +11 -4
  64. data/lib/mistri/stores/memory.rb +21 -2
  65. data/lib/mistri/sub_agent/execution.rb +81 -0
  66. data/lib/mistri/sub_agent/runtime.rb +297 -0
  67. data/lib/mistri/sub_agent.rb +124 -87
  68. data/lib/mistri/task_output.rb +24 -6
  69. data/lib/mistri/tool.rb +93 -13
  70. data/lib/mistri/tool_arguments.rb +377 -0
  71. data/lib/mistri/tool_call.rb +43 -9
  72. data/lib/mistri/tool_context.rb +4 -2
  73. data/lib/mistri/tool_executor.rb +117 -26
  74. data/lib/mistri/tool_result.rb +15 -10
  75. data/lib/mistri/tools/edit_file.rb +62 -8
  76. data/lib/mistri/tools.rb +41 -4
  77. data/lib/mistri/transport.rb +149 -44
  78. data/lib/mistri/usage.rb +65 -13
  79. data/lib/mistri/version.rb +1 -1
  80. data/lib/mistri/workspace/active_record.rb +190 -5
  81. data/lib/mistri/workspace/directory.rb +28 -8
  82. data/lib/mistri/workspace/memory.rb +34 -9
  83. data/lib/mistri/workspace/single.rb +62 -5
  84. data/lib/mistri/workspace.rb +39 -0
  85. data/lib/mistri.rb +6 -1
  86. data/mistri.gemspec +34 -0
  87. metadata +31 -3
@@ -0,0 +1,324 @@
1
+ # Tool contracts
2
+
3
+ A Mistri tool is application code exposed to model output. Treat that boundary
4
+ like an HTTP endpoint: declare what it accepts, authorize the acting context,
5
+ and make writes idempotent or reconcilable.
6
+
7
+ This guide covers local tools. Remote MCP tools cross the same execution
8
+ boundary; see [MCP](mcp.md) for the additional server contract.
9
+
10
+ ## Define a tool
11
+
12
+ ```ruby
13
+ charge_card = Mistri::Tool.define(
14
+ "charge_card", "Charges a saved payment method.",
15
+ schema: lambda {
16
+ string :payment_method_id, "Saved payment method ID", required: true
17
+ number :amount_usd, "Amount in US dollars", required: true
18
+ },
19
+ needs_approval: ->(args) { args.fetch("amount_usd") >= 500 },
20
+ ) do |args, context|
21
+ Payments.charge!(
22
+ customer: context.app.fetch(:customer),
23
+ payment_method_id: args.fetch("payment_method_id"),
24
+ amount_usd: args.fetch("amount_usd"),
25
+ )
26
+ end
27
+ ```
28
+
29
+ The handler may accept only `args`, or `args` and `context`. `context.app` is
30
+ the object passed as `Mistri.agent(context:)`; it is the usual place for the
31
+ acting user, tenant, or request-scoped authorization context.
32
+
33
+ Handlers may return:
34
+
35
+ - a String, delivered to the model as text;
36
+ - a Hash, number, boolean, nil, or data Array, serialized as JSON or empty text;
37
+ - an Array made only of Strings and Mistri content blocks, delivered as
38
+ multiple content blocks, including images;
39
+ - `Mistri::ToolResult`, which separates model content, host-only UI, and an
40
+ explicit failure fact.
41
+
42
+ ## The execution boundary
43
+
44
+ A completed model call moves through these phases in order:
45
+
46
+ 1. Mistri verifies the provider's call envelope and pairing metadata.
47
+ 2. The argument value is copied into bounded, immutable JSON owned by Mistri.
48
+ 3. The tool's explicit `argument_normalizer`, if any, runs once.
49
+ 4. Core schema validation and the tool's host validator run.
50
+ 5. `before_tool` evaluates current application policy.
51
+ 6. `needs_approval` decides whether the prepared call must park.
52
+ 7. A free call emits `:tool_started` and invokes the handler.
53
+ 8. `after_tool` may rewrite the result, which then persists and emits
54
+ `:tool_result`.
55
+
56
+ Malformed or invalid calls do not reach policy or execution. The model receives
57
+ a bounded error result it can correct, while valid sibling calls in the same
58
+ turn continue. Results settle in the model's call order even when handlers run
59
+ concurrently.
60
+
61
+ An approved call is revalidated against the current tool definition and passes
62
+ through `before_tool` again on `resume`. Its normalizer and approval predicate
63
+ do not run again: the human approved the already prepared arguments.
64
+
65
+ ## Schema forms
66
+
67
+ The DSL builds a JSON Schema 2020-12 object:
68
+
69
+ ```ruby
70
+ search = Mistri::Tool.define("search", "Searches products.", schema: lambda {
71
+ string :query, "Search text", required: true
72
+ integer :limit, "Maximum result count"
73
+ array :tags, "Required tags", items: { type: "string" }
74
+ object :filters, "Structured filters" do
75
+ string :country, "ISO country code"
76
+ boolean :in_stock, "Only products in stock"
77
+ end
78
+ }) do |args|
79
+ filters = args.fetch("filters", {})
80
+ Catalog.search(
81
+ query: args.fetch("query"),
82
+ limit: args["limit"],
83
+ tags: args.fetch("tags", []),
84
+ country: filters["country"],
85
+ in_stock: filters["in_stock"],
86
+ )
87
+ end
88
+ ```
89
+
90
+ Use `input_schema:` for a raw schema:
91
+
92
+ ```ruby
93
+ schema = {
94
+ type: "object",
95
+ properties: {
96
+ "invoice_id" => { type: "string" },
97
+ },
98
+ required: ["invoice_id"],
99
+ additionalProperties: false,
100
+ }
101
+
102
+ Mistri::Tool.define("pay_invoice", "Pays one invoice.", input_schema: schema) do |args|
103
+ Invoices.pay!(args.fetch("invoice_id"))
104
+ end
105
+ ```
106
+
107
+ The root must be an object schema. `Tool.define` without `schema:` or
108
+ `input_schema:` creates a closed no-argument tool; model-supplied fields are
109
+ rejected.
110
+
111
+ ### Open and closed objects
112
+
113
+ Supplied object schemas follow JSON Schema's default-open semantics. Declared
114
+ properties do not automatically reject additional keys. Extract the fields you
115
+ intend to use rather than mass-assigning the entire model hash.
116
+
117
+ Set `additionalProperties: false` in a raw schema when extra keys must fail.
118
+
119
+ A nested object declared without a block is intentionally freeform:
120
+
121
+ ```ruby
122
+ object :config, "Chart-library configuration", required: true
123
+ ```
124
+
125
+ It accepts any JSON object, including `{}`. This is useful when Mistri should
126
+ not prescribe keys owned by another library. A bare top-level `Schema.build`
127
+ raises because it communicates no argument contract.
128
+
129
+ Freeform objects cannot become strict constrained output without changing their
130
+ meaning. `Schema.strict` and task planning reject that shape instead of silently
131
+ narrowing it to `{}`.
132
+
133
+ ### Portable validation subset
134
+
135
+ Mistri's zero-dependency validator enforces:
136
+
137
+ - JSON `type`, including type unions;
138
+ - `enum`;
139
+ - `required` and declared `properties`;
140
+ - one-schema array `items`;
141
+ - tuple array `prefixItems`;
142
+ - boolean schemas;
143
+ - boolean or schema-valued `additionalProperties`.
144
+
145
+ Other standard keywords remain provider-facing generation guidance unless a
146
+ complete validator is supplied. Examples include `minimum`, `pattern`,
147
+ `format`, length constraints, and applicators such as `anyOf`.
148
+
149
+ An argument-applicable, non-empty `patternProperties` requires complete
150
+ authority because approximating its key and value interactions would be
151
+ unsafe. External references are not resolved.
152
+
153
+ Task output is stricter than tool input: task planning rejects assertions that
154
+ local validation cannot guarantee. A task promises a validated value; a tool
155
+ server can still enforce its own broader contract after Mistri's local subset.
156
+
157
+ ## Host validation
158
+
159
+ Use `argument_validator:` for domain rules that supplement core validation:
160
+
161
+ ```ruby
162
+ invoice = Mistri::Tool.define(
163
+ "pay_invoice", "Pays an approved invoice.",
164
+ input_schema: invoice_schema,
165
+ argument_validator: lambda { |args, _schema|
166
+ if args.fetch("invoice_id").start_with?("inv_")
167
+ []
168
+ else
169
+ ["$.invoice_id must identify an invoice"]
170
+ end
171
+ },
172
+ ) do |args|
173
+ Invoices.pay!(args.fetch("invoice_id"))
174
+ end
175
+ ```
176
+
177
+ The validator receives the deeply frozen arguments and canonical schema. It
178
+ must return an Array of Strings. Core validation runs first and cannot be
179
+ weakened.
180
+
181
+ Use `complete_argument_validator:` only when the host validator implements the
182
+ whole schema, including interactions outside Mistri's portable subset. It has
183
+ the same `(args, schema)` signature and is mutually exclusive with
184
+ `argument_validator:`.
185
+
186
+ Validator errors are sent back to the model. Describe the expected contract;
187
+ do not echo received secrets or field values.
188
+
189
+ ## Explicit normalization
190
+
191
+ Mistri does not coerce model input. If a tool intentionally accepts a legacy
192
+ alias or representation, declare that compatibility policy on the tool:
193
+
194
+ ```ruby
195
+ normalizer = lambda do |args|
196
+ next args unless args.key?("account")
197
+ raise ArgumentError, "choose account or account_id" if args.key?("account_id")
198
+
199
+ normalized = args.dup
200
+ normalized["account_id"] = normalized.delete("account")
201
+ normalized
202
+ end
203
+
204
+ tool = Mistri::Tool.define(
205
+ "lookup_account", "Looks up one account.",
206
+ schema: -> { string :account_id, "Account ID", required: true },
207
+ argument_normalizer: normalizer,
208
+ ) do |args|
209
+ Accounts.find(args.fetch("account_id"))
210
+ end
211
+ ```
212
+
213
+ The normalizer must return a Hash and should be pure, deterministic, and
214
+ idempotent. Its output is what validation, policy, approval, and the handler
215
+ see.
216
+
217
+ Direct `Tool#call` is a trusted host invocation. It applies the normalizer for
218
+ compatibility, but the Agent's model-input ownership and validation boundary is
219
+ not involved.
220
+
221
+ ## Policy hooks
222
+
223
+ `before_tool` can block a call with a model-readable reason:
224
+
225
+ ```ruby
226
+ before_tool = lambda do |call, context|
227
+ next "customer cannot perform this action" unless context.app.fetch(:customer).active?
228
+ next "currency is not enabled" unless call.arguments.fetch("currency") == "USD"
229
+ end
230
+
231
+ agent = Mistri.agent("claude-opus-4-8", tools: tools, before_tool: before_tool)
232
+ ```
233
+
234
+ The hook also runs when an approved call resumes, so long-lived approval does
235
+ not bypass current authorization. A raised hook error blocks conservatively.
236
+
237
+ `after_tool` receives `(call, result, context)` and may return a replacement
238
+ result. Returning nil keeps the original. If the original result is an error,
239
+ rewriting its text cannot relabel it as success.
240
+
241
+ ## Results and UI
242
+
243
+ Use `Mistri::ToolResult` when the model and host need different views:
244
+
245
+ ```ruby
246
+ Mistri::Tool.define("edit_page", "Applies a page edit.", schema: lambda {
247
+ object :changes, "Page changes", required: true
248
+ }) do |args|
249
+ page = Pages.apply(args.fetch("changes"))
250
+ Mistri::ToolResult.new(
251
+ content: "Saved the page.",
252
+ ui: { "html" => page.html, "revision" => page.lock_version },
253
+ )
254
+ end
255
+ ```
256
+
257
+ `ui` persists on the tool message and appears on the `:tool_result` event, but
258
+ is never sent to a provider.
259
+
260
+ Non-text tool-result content remains provider-neutral in the Session. Anthropic
261
+ can receive image blocks in a tool result. The current OpenAI and Gemini
262
+ function-result serializers have no such encoding and send an explicit
263
+ non-text-omission marker alongside the text instead.
264
+
265
+ Return `error: true` for an expected failure the model should handle:
266
+
267
+ ```ruby
268
+ account || Mistri::ToolResult.new(
269
+ content: "Account not found.",
270
+ error: true,
271
+ )
272
+ ```
273
+
274
+ Mistri also marks unknown tools, validation and policy rejection, handler
275
+ exceptions, timeouts, pre-invocation interruption, failed result hooks,
276
+ crash-healed calls, and MCP `isError` results. Human denial is an expected
277
+ control outcome, not an execution failure.
278
+
279
+ `event.tool_error` and `event.message.tool_error?` expose the fact without
280
+ parsing prose. A legacy persisted result without the field remains unknown;
281
+ Mistri does not infer historical success from text.
282
+
283
+ An error flag does not make a replay safe. A handler may have committed a write
284
+ before raising or timing out. Mistri never mechanically replays the same call
285
+ because it is marked failed, but the model may issue another call. The host
286
+ still owns idempotency and reconciliation.
287
+
288
+ ## Hand off the turn
289
+
290
+ `ends_turn: true` ends the run after the tool executes instead of asking the
291
+ model for another response. This is useful for `ask_user`, escalation, or a
292
+ structural handoff:
293
+
294
+ ```ruby
295
+ ask_user = Mistri::Tool.define(
296
+ "ask_user", "Asks the human and waits.",
297
+ ends_turn: true,
298
+ schema: -> { string :question, "Question", required: true },
299
+ ) do |_args|
300
+ "Question presented to the user."
301
+ end
302
+ ```
303
+
304
+ `result.handed_off?` reports that the model's floor moved away. It does not
305
+ claim the tool succeeded; inspect the tool result separately. The human answer
306
+ arrives as the next `run` input.
307
+
308
+ ## Resource limits
309
+
310
+ Completed arguments are bounded at 8 MiB, 64 levels, 10,000 JSON nodes, and 64
311
+ KiB per numeric token. Encoded Anthropic and OpenAI arguments receive a linear
312
+ lexical pass before JSON parsing. Gemini's enclosing SSE record receives the
313
+ same structural preflight before its argument object is canonicalized.
314
+
315
+ These are safety boundaries, not business validation. Keep tool schemas narrow,
316
+ validate domain rules explicitly, and return references rather than embedding
317
+ unbounded data in a tool call.
318
+
319
+ ## Related guides
320
+
321
+ - [Sessions and control](sessions.md)
322
+ - [MCP](mcp.md)
323
+ - [Reliability](reliability.md)
324
+ - [Upgrading](../UPGRADING.md)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Fire-and-forget human approval. The run suspends without executing the
4
+ # gated tool; the decision is a session write (here immediate, in production
5
+ # a controller action days later); resume settles it and finishes. Sessions
6
+ # live in JSONL files so every step could happen in a different process.
7
+ # Needs ANTHROPIC_API_KEY.
8
+ #
9
+ # ruby examples/approval.rb
10
+
11
+ require "mistri"
12
+ require "tmpdir"
13
+
14
+ Dir.mktmpdir do |dir|
15
+ store = Mistri::Stores::JSONL.new(dir)
16
+ session = Mistri::Session.new(store:)
17
+
18
+ send_gift = Mistri::Tool.define(
19
+ "send_gift", "Sends a real gift.", needs_approval: true,
20
+ schema: -> { string :to, "Recipient", required: true }
21
+ ) do |args|
22
+ "gift queued for #{args["to"]}"
23
+ end
24
+
25
+ agent = Mistri.agent("claude-opus-4-8", tools: [send_gift], session:)
26
+ result = agent.run("Send a welcome gift to Ana.")
27
+ puts "suspended: #{result.awaiting_approval?} (nothing executed)"
28
+
29
+ # Any process can decide: a bare session is enough.
30
+ Mistri::Session.new(store:, id: session.id).approve(result.pending.first.id)
31
+
32
+ reloaded = Mistri::Session.new(store:, id: session.id)
33
+ resumed = Mistri.agent("claude-opus-4-8", tools: [send_gift], session: reloaded).resume
34
+ puts "resumed: #{resumed.status}"
35
+ puts resumed.text
36
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Give an agent a real browser in one line: Playwright's official MCP
4
+ # server over the stdio wire. The allowlist grants only what this agent
5
+ # needs, and needs_approval gates could ride any of it. Needs
6
+ # ANTHROPIC_API_KEY, node, and Chrome.
7
+ #
8
+ # ruby examples/browser.rb
9
+
10
+ require "mistri"
11
+
12
+ browser = Mistri::MCP::Client.new(
13
+ command: ["npx", "-y", "@playwright/mcp@latest", "--browser", "chrome", "--headless"],
14
+ read_timeout: 180
15
+ )
16
+
17
+ begin
18
+ tools = Mistri::MCP.tools(browser, prefix: "web",
19
+ allow: %w[browser_navigate browser_snapshot])
20
+
21
+ agent = Mistri.agent("claude-opus-4-8", tools: tools,
22
+ system: "Use the browser tools to answer. Be brief.")
23
+
24
+ puts agent.run("Open https://example.com and report the main heading.").text
25
+ ensure
26
+ browser.close
27
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The landing-page shape: one value (a database column in production, a
4
+ # string here) edited in place through the document tools. A host can wrap the
5
+ # result in ToolResult when it also needs a separate UI payload.
6
+ # Needs ANTHROPIC_API_KEY.
7
+ #
8
+ # ruby examples/page_editor.rb
9
+
10
+ require "mistri"
11
+
12
+ page = +<<~HTML
13
+ <header>
14
+ <h1>Placeholder Headline</h1>
15
+ <p class="tagline">Ship faster.</p>
16
+ </header>
17
+ HTML
18
+
19
+ workspace = Mistri::Workspace::Single.new(
20
+ read: -> { page.dup },
21
+ write: ->(html) { page.replace(html) },
22
+ path: "hero.html"
23
+ )
24
+
25
+ agent = Mistri.agent("claude-opus-4-8",
26
+ tools: Mistri::Tools.files(workspace),
27
+ system: "Edit the page with the document tools. Read before editing.")
28
+
29
+ agent.run("Change the h1 headline to: Gifts that land.")
30
+
31
+ puts page
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A tool-using agent, streamed. Needs ANTHROPIC_API_KEY.
4
+ #
5
+ # ruby examples/quickstart.rb
6
+
7
+ require "mistri"
8
+
9
+ weather = Mistri::Tool.define(
10
+ "get_weather", "Current weather for a city.",
11
+ schema: -> { string :city, "City name", required: true }
12
+ ) do |args|
13
+ "34C and clear in #{args["city"]}"
14
+ end
15
+
16
+ agent = Mistri.agent("claude-opus-4-8", tools: [weather])
17
+
18
+ agent.run("What should I wear in Lahore today? One sentence.") do |event|
19
+ print event.delta if event.type == :text_delta
20
+ end
21
+ puts
@@ -41,14 +41,18 @@ module Mistri
41
41
 
42
42
  private
43
43
 
44
- # MySQL TEXT caps at 64KB, which a single large tool result can blow
45
- # through; MEDIUMTEXT holds 16MB. Postgres text is unbounded.
44
+ # A parallel tool turn can legitimately exceed MySQL MEDIUMTEXT even
45
+ # though each call stays inside its own input limit. Postgres text is unbounded.
46
46
  def payload_size
47
47
  adapter = ::ActiveRecord::Base.connection_db_config.adapter.to_s
48
- adapter.match?(/mysql|trilogy/) ? ", size: :medium" : ""
48
+ payload_size_for(adapter)
49
49
  rescue StandardError
50
50
  ""
51
51
  end
52
+
53
+ def payload_size_for(adapter)
54
+ adapter.to_s.match?(/mysql|trilogy/) ? ", size: :long" : ""
55
+ end
52
56
  end
53
57
  end
54
58
  end
@@ -7,8 +7,8 @@ class Create<%= table_name.camelize %> < ActiveRecord::Migration[<%= ActiveRecor
7
7
  t.timestamps
8
8
  end
9
9
 
10
- # One session has one writer; a colliding append must raise, never
11
- # silently reorder entries.
10
+ # The unique position is the optimistic concurrency boundary for
11
+ # independent appenders; a collision retries at the next position.
12
12
  add_index :<%= table_name %>, [:session_id, :position], unique: true
13
13
  end
14
14
  end
@@ -12,7 +12,7 @@ class Create<%= table_name.camelize %> < ActiveRecord::Migration[<%= ActiveRecor
12
12
 
13
13
  t.string :client_id
14
14
  t.string :client_secret
15
- t.string :token_endpoint
15
+ t.string :issuer
16
16
  t.string :token_auth_method
17
17
  t.text :access_token
18
18
  t.text :refresh_token
@@ -4,15 +4,19 @@
4
4
  class <%= class_name %> < ApplicationRecord
5
5
  encrypts :access_token, :refresh_token, :client_secret
6
6
 
7
+ def self.mcp_allow_non_public = nil
8
+
7
9
  # Begin the OAuth flow: persists a pending connection and returns it with
8
10
  # the URL to send the user to.
9
- def self.connect(name:, url:, client_name:, redirect_uri:, scope: nil)
11
+ def self.connect(name:, url:, client_name:, redirect_uri:, scope: nil, issuer: nil)
10
12
  flow = Mistri::MCP::OAuth.start(url: url, client_name: client_name,
11
- redirect_uri: redirect_uri, scope: scope)
13
+ redirect_uri: redirect_uri, scope: scope,
14
+ issuer: issuer,
15
+ allow_non_public: mcp_allow_non_public)
12
16
  connection = create!(name: name, url: url, status: "pending",
13
17
  state: flow["state"], code_verifier: flow["code_verifier"],
14
18
  client_id: flow["client_id"], client_secret: flow["client_secret"],
15
- token_endpoint: flow["token_endpoint"],
19
+ issuer: flow["issuer"],
16
20
  token_auth_method: flow["token_auth_method"],
17
21
  redirect_uri: flow["redirect_uri"])
18
22
  [connection, flow["authorize_url"]]
@@ -25,10 +29,11 @@ class <%= class_name %> < ApplicationRecord
25
29
  code_verifier: connection.code_verifier,
26
30
  client_id: connection.client_id,
27
31
  client_secret: connection.client_secret,
28
- token_endpoint: connection.token_endpoint,
32
+ issuer: connection.issuer,
29
33
  token_auth_method: connection.token_auth_method,
30
34
  resource: connection.url,
31
- redirect_uri: connection.redirect_uri)
35
+ redirect_uri: connection.redirect_uri,
36
+ allow_non_public: mcp_allow_non_public)
32
37
  connection.update!(status: "connected", state: nil, code_verifier: nil,
33
38
  access_token: tokens["access_token"],
34
39
  refresh_token: tokens["refresh_token"],
@@ -37,7 +42,8 @@ class <%= class_name %> < ApplicationRecord
37
42
  end
38
43
 
39
44
  def client
40
- Mistri::MCP::Client.new(url: url, token: -> { bearer_token })
45
+ Mistri::MCP::Client.new(url: url, token: -> { bearer_token },
46
+ allow_non_public: self.class.mcp_allow_non_public)
41
47
  end
42
48
 
43
49
  def tools(**options)
@@ -54,8 +60,9 @@ class <%= class_name %> < ApplicationRecord
54
60
  def refresh!
55
61
  tokens = Mistri::MCP::OAuth.refresh(refresh_token: refresh_token,
56
62
  client_id: client_id, client_secret: client_secret,
57
- token_endpoint: token_endpoint,
58
- token_auth_method: token_auth_method, resource: url)
63
+ issuer: issuer, token_auth_method: token_auth_method,
64
+ resource: url,
65
+ allow_non_public: self.class.mcp_allow_non_public)
59
66
  update!(access_token: tokens["access_token"],
60
67
  refresh_token: tokens["refresh_token"] || refresh_token,
61
68
  expires_at: tokens["expires_at"], scope: tokens["scope"] || scope)