omni_agent 0.1.10 → 0.2.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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/README.md +100 -2
  4. data/Rakefile +4 -0
  5. data/app/jobs/omni_agent/embed_job.rb +10 -0
  6. data/app/jobs/omni_agent/ingest_job.rb +12 -0
  7. data/docs/agent/callbacks.md +60 -0
  8. data/docs/agent/delegation.md +56 -0
  9. data/docs/agent/provider.md +93 -0
  10. data/docs/agent/run-entrypoints.md +81 -0
  11. data/docs/agent/streaming.md +62 -0
  12. data/docs/agent/structured-output.md +80 -0
  13. data/docs/configuration.md +68 -0
  14. data/docs/evals/caching.md +21 -0
  15. data/docs/evals/generating-and-running.md +87 -0
  16. data/docs/evals/golden-sets.md +32 -0
  17. data/docs/evals/judge-provider.md +20 -0
  18. data/docs/generators.md +143 -0
  19. data/docs/index.md +98 -0
  20. data/docs/intro.md +93 -0
  21. data/docs/mcp/authentication.md +226 -0
  22. data/docs/mcp/defining-servers.md +196 -0
  23. data/docs/mcp/transports.md +166 -0
  24. data/docs/rag.md +372 -0
  25. data/docs/stock-agents/ocr.md +52 -0
  26. data/docs/stock-agents/overview.md +91 -0
  27. data/docs/supported-providers.md +47 -0
  28. data/docs/tools.md +171 -0
  29. data/exe/omni_agent +48 -10
  30. data/lib/generators/omni_agent/chunks/chunks_generator.rb +130 -0
  31. data/lib/generators/omni_agent/embeddings/embeddings_generator.rb +72 -0
  32. data/lib/generators/omni_agent/install/install_generator.rb +78 -1
  33. data/lib/generators/omni_agent/mcp_server/mcp_server_generator.rb +4 -0
  34. data/lib/generators/omni_agent/stock_agent/stock_agent_generator.rb +73 -0
  35. data/lib/generators/omni_agent/vector_store.rb +113 -0
  36. data/lib/omni_agent/agent.rb +168 -39
  37. data/lib/omni_agent/chunkable.rb +181 -0
  38. data/lib/omni_agent/chunkers/hierarchical.rb +20 -0
  39. data/lib/omni_agent/chunkers/markdown.rb +41 -0
  40. data/lib/omni_agent/chunkers/recursive.rb +53 -0
  41. data/lib/omni_agent/chunkers.rb +31 -0
  42. data/lib/omni_agent/configuration.rb +14 -1
  43. data/lib/omni_agent/docs.rb +32 -0
  44. data/lib/omni_agent/embeddable.rb +131 -0
  45. data/lib/omni_agent/engine.rb +11 -0
  46. data/lib/omni_agent/errors.rb +15 -0
  47. data/lib/omni_agent/loaders/html.rb +21 -0
  48. data/lib/omni_agent/loaders/markdown.rb +18 -0
  49. data/lib/omni_agent/loaders/pdf.rb +25 -0
  50. data/lib/omni_agent/loaders/text.rb +15 -0
  51. data/lib/omni_agent/loaders.rb +68 -0
  52. data/lib/omni_agent/mcp/authenticator.rb +5 -2
  53. data/lib/omni_agent/mcp/oauth/access_token.rb +60 -0
  54. data/lib/omni_agent/mcp/oauth/http_client.rb +52 -0
  55. data/lib/omni_agent/mcp/oauth/introspection_verifier.rb +43 -0
  56. data/lib/omni_agent/mcp/oauth/jwt_verifier.rb +122 -0
  57. data/lib/omni_agent/mcp/oauth/resource_server.rb +166 -0
  58. data/lib/omni_agent/mcp/oauth.rb +18 -0
  59. data/lib/omni_agent/mcp/rack_app.rb +31 -7
  60. data/lib/omni_agent/mcp/server.rb +22 -5
  61. data/lib/omni_agent/providers/base.rb +14 -0
  62. data/lib/omni_agent/providers/mock.rb +89 -4
  63. data/lib/omni_agent/providers/ollama.rb +4 -0
  64. data/lib/omni_agent/providers/openai.rb +33 -2
  65. data/lib/omni_agent/providers/response.rb +6 -1
  66. data/lib/omni_agent/providers.rb +7 -0
  67. data/lib/omni_agent/schema.rb +200 -0
  68. data/lib/omni_agent/semantic_search.rb +45 -0
  69. data/lib/omni_agent/stock_agent.rb +59 -0
  70. data/lib/omni_agent/stock_agents/ocr/prompt.md.erb +6 -0
  71. data/lib/omni_agent/stock_agents/ocr.rb +70 -0
  72. data/lib/omni_agent/stock_agents.rb +26 -0
  73. data/lib/omni_agent/tool.rb +7 -107
  74. data/lib/omni_agent/version.rb +1 -1
  75. data/lib/omni_agent.rb +15 -1
  76. data/lib/tasks/omni_agent_chunks_tasks.rake +16 -0
  77. data/lib/tasks/omni_agent_embeddings_tasks.rake +15 -0
  78. metadata +55 -8
  79. data/app/assets/stylesheets/omni_agent/application.css +0 -15
  80. data/app/controllers/omni_agent/application_controller.rb +0 -4
  81. data/app/helpers/omni_agent/application_helper.rb +0 -4
  82. data/app/jobs/omni_agent/application_job.rb +0 -4
  83. data/app/mailers/omni_agent/application_mailer.rb +0 -6
  84. data/app/models/omni_agent/application_record.rb +0 -5
  85. data/app/views/layouts/omni_agent/application.html.erb +0 -17
@@ -0,0 +1,226 @@
1
+ <!-- Generated from omniagent-docs/docs/mcp/authentication.mdx by `bundle exec rake docs:sync`. Do not edit by hand. -->
2
+ <!-- Online version: https://omniagent.activetaskers.com/docs/mcp/authentication -->
3
+
4
+ # Authentication
5
+
6
+ An MCP server mounted over HTTP lets anyone who can reach the URL run your tools. For that reason, **every server must declare `authenticate`**. A server without it raises `OmniAgent::MCPError` on the first HTTP request. To make a server public on purpose, write `authenticate :none`.
7
+
8
+ Authentication returns a **principal**: the identity of the caller. The principal is passed to [`authorize_tool`](defining-servers.md#per-principal-tool-access), to the [`context`](defining-servers.md#tool-context) block, and to tools as `context[:mcp_principal]`.
9
+
10
+ Requests that fail authentication get `401 Unauthorized` with `WWW-Authenticate: Bearer realm="<server_name>"`. [OAuth 2.1](#oauth-21) servers add more detail to that header.
11
+
12
+ ## Static Bearer Tokens
13
+
14
+ ```ruby
15
+ authenticate :bearer, tokens: -> { ENV.fetch("SUPPORT_MCP_TOKENS", "").split(",") }
16
+ ```
17
+
18
+ Clients send `Authorization: Bearer <token>`. `tokens:` accepts:
19
+
20
+ * **An Array**: Any listed token is accepted. The principal is `:bearer_token`.
21
+ * **A Hash of `principal => token`**: The principal is the matching key. Use it to tell callers apart:
22
+
23
+ ```ruby
24
+ authenticate :bearer, tokens: -> { { "ci" => ENV["CI_MCP_TOKEN"], "ops" => ENV["OPS_MCP_TOKEN"] } }
25
+ ```
26
+
27
+ * **A Proc that returns either of the above**: The proc runs on every request, so you can rotate tokens in the environment or in credentials without restarting the server. Use a proc whenever you read tokens from `ENV`.
28
+
29
+ Tokens are compared with `ActiveSupport::SecurityUtils.secure_compare`. Every configured token is checked, with no early exit. Blank tokens are ignored, so an unset environment variable never matches an empty `Authorization` header.
30
+
31
+ ## Custom Authentication
32
+
33
+ Pass a block to authenticate against your own data. The block receives an `OmniAgent::MCP::Request` (a `Rack::Request` that also has `#bearer_token`). Return the principal, or `nil`/`false` to reject the request:
34
+
35
+ ```ruby
36
+ authenticate do |request|
37
+ User.find_by(api_token: request.bearer_token)
38
+ end
39
+ ```
40
+
41
+ The returned user is then the principal everywhere:
42
+
43
+ ```ruby
44
+ authorize_tool { |tool_class, user| user.admin? || !tool_class.tags.include?(:admin) }
45
+ context { |user, _request| { current_user: user } }
46
+ ```
47
+
48
+ ## OAuth 2.1
49
+
50
+ `authenticate :oauth` implements the [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization). Your MCP server is an OAuth 2.1 **resource server**: an external **authorization server** (Auth0, Keycloak, Okta, Entra ID, WorkOS, Doorkeeper, ...) signs users in and issues access tokens, and OmniAgent validates those tokens on every request. MCP clients such as Claude Code, Claude Desktop and the MCP Inspector discover the authorization server and run the OAuth flow by themselves, so users never copy tokens by hand.
51
+
52
+ ```ruby
53
+ class SupportServer < OmniAgent::MCP::Server
54
+ authenticate :oauth,
55
+ resource: "https://app.example.com/mcp/support",
56
+ authorization_servers: "https://auth.example.com",
57
+ scopes: %w[mcp:tools],
58
+ jwks_uri: "https://auth.example.com/.well-known/jwks.json" do |access_token, _request|
59
+ User.find_by(oauth_subject: access_token.subject)
60
+ end
61
+ end
62
+ ```
63
+
64
+ OmniAgent does not include an authorization server. Use your identity provider, or a gem such as Doorkeeper in your app. The MCP specification expects it to support PKCE and to accept the `resource` parameter ([RFC 8707](https://www.rfc-editor.org/rfc/rfc8707)), so that tokens name your server as their audience.
65
+
66
+ ### Options
67
+
68
+ * **`resource:`** (required): The canonical URL of this MCP endpoint, as clients connect to it. Tokens must list it in `aud`. It is configured rather than read from the `Host` header, so a token for another server cannot be replayed by changing that header.
69
+ * **`authorization_servers:`** (required): One or more issuer URLs. They are published in the metadata, and a token's `iss` must be one of them.
70
+ * **`scopes:`**: Scopes every token must have. Defaults to none. A valid token that lacks one gets `403` with `error="insufficient_scope"`.
71
+ * **`scopes_supported:`**: Scopes advertised in the metadata. Defaults to `scopes:`.
72
+ * **`audience:`**: Accept this `aud` value instead of `resource:`. Use it when your authorization server issues a fixed API identifier (for example `api://support`).
73
+ * **Exactly one way to validate tokens**: `jwks_uri:`, `introspection:` or `verify_token:`. See below.
74
+
75
+ `resource:`, `authorization_servers:`, `audience:` and the `introspection:` values also accept a Proc, which runs on every request:
76
+
77
+ ```ruby
78
+ authenticate :oauth, resource: -> { ENV.fetch("SUPPORT_MCP_URL") }, ...
79
+ ```
80
+
81
+ ### Validating Tokens
82
+
83
+ **JWT access tokens** ([RFC 9068](https://www.rfc-editor.org/rfc/rfc9068)), checked against the authorization server's public keys. Add `gem "jwt"` to your Gemfile:
84
+
85
+ ```ruby
86
+ authenticate :oauth, ..., jwks_uri: "https://auth.example.com/.well-known/jwks.json"
87
+ ```
88
+
89
+ The signature, `exp`, `nbf` and `iss` are checked. Tokens must contain `exp` and `iss`. Only asymmetric algorithms (`RS*`, `PS*`, `ES*`) are accepted; change them with `algorithms:`. The key set is cached for 5 minutes. A token with an unknown `kid` refetches it (key rotation), at most every 30 seconds. One request refreshes the keys while the others keep using the cached ones. If the authorization server cannot be reached, OmniAgent keeps using the cached keys, or, if it has none yet, rejects requests with `401` right away and tries again after 30 seconds.
90
+
91
+ **Token introspection** ([RFC 7662](https://www.rfc-editor.org/rfc/rfc7662)), for opaque tokens. Every request asks the authorization server whether the token is active:
92
+
93
+ ```ruby
94
+ authenticate :oauth, ...,
95
+ introspection: {
96
+ url: "https://auth.example.com/oauth/introspect",
97
+ client_id: -> { Rails.application.credentials.mcp_client_id },
98
+ client_secret: -> { Rails.application.credentials.mcp_client_secret }
99
+ }
100
+ ```
101
+
102
+ > **Warning: Introspection responses must include `aud`**
103
+ >
104
+ > Every token must name your server in `aud` (see below), but many introspection endpoints leave it out. Doorkeeper, for example, does not return it by default, so every request gets `401` and nothing explains why. Add it to the response. With Doorkeeper, use `custom_introspection_response`:
105
+ >
106
+ > ```ruby
107
+ > # config/initializers/doorkeeper.rb
108
+ > Doorkeeper.configure do
109
+ > custom_introspection_response do |_token, _context|
110
+ > { aud: "https://app.example.com/mcp/support" }
111
+ > end
112
+ > end
113
+ > ```
114
+
115
+ An MCP session sends many requests, and each one calls the introspection endpoint. If that becomes a problem, cache the result yourself with `verify_token:`. A revoked token keeps working until its cache entry expires, so keep the expiry short:
116
+
117
+ ```ruby
118
+ introspector = OmniAgent::MCP::OAuth::IntrospectionVerifier.new(
119
+ url: "https://auth.example.com/oauth/introspect",
120
+ client_id: -> { Rails.application.credentials.mcp_client_id },
121
+ client_secret: -> { Rails.application.credentials.mcp_client_secret }
122
+ )
123
+
124
+ authenticate :oauth, ...,
125
+ verify_token: ->(token) {
126
+ Rails.cache.fetch("mcp_token/#{Digest::SHA256.hexdigest(token)}", expires_in: 1.minute) do
127
+ introspector.call(token)
128
+ end
129
+ }
130
+ ```
131
+
132
+ **Custom verification**, when you already have the logic. The block receives the raw token (and, optionally, the request) and returns the token's claims as a Hash, or `nil` to reject it:
133
+
134
+ ```ruby
135
+ authenticate :oauth, ...,
136
+ verify_token: ->(token) {
137
+ record = OAuthToken.active.find_by(digest: Digest::SHA256.hexdigest(token))
138
+ record && { "sub" => record.user_id.to_s, "aud" => record.resource,
139
+ "scope" => record.scopes, "exp" => record.expires_at.to_i }
140
+ }
141
+ ```
142
+
143
+ Whatever the method, OmniAgent then checks the claims: `aud` must contain `resource:` (or `audience:`), `exp` and `nbf` must be current (30 seconds of clock skew allowed), `iss` (if present) must be one of `authorization_servers:`, and `scope` (or `scp`) must contain every scope in `scopes:`. If the authorization server cannot be reached, the request gets `401` and the error is logged.
144
+
145
+ Exceptions raised by your own code, in `verify_token:` or in the block that returns the principal (for example a database error in `User.find_by`), are not caught. The request gets `500`, not `401`.
146
+
147
+ ### The Principal and the Access Token
148
+
149
+ The validated token is an `OmniAgent::MCP::OAuth::AccessToken`, with `#subject`, `#client_id`, `#issuer`, `#audience`, `#scopes`, `#scope?(*names)`, `#expires_at` and `#claims`.
150
+
151
+ Without a block, the access token is the principal. With a block, the block's return value is the principal; return `nil` to reject the request. Either way, the token is available as `access_token` inside `authorize_tool` and `context`, and to tools as `context[:mcp_access_token]`. Use it to require extra scopes for some tools:
152
+
153
+ ```ruby
154
+ authorize_tool { |tool_class, _user| !tool_class.tags.include?(:admin) || access_token.scope?("mcp:admin") }
155
+ ```
156
+
157
+ ### Discovery
158
+
159
+ Clients find the authorization server through Protected Resource Metadata ([RFC 9728](https://www.rfc-editor.org/rfc/rfc9728)). OmniAgent serves it under the endpoint, with no authentication and with CORS enabled for browser-based clients:
160
+
161
+ ```bash
162
+ curl https://app.example.com/mcp/support/.well-known/oauth-protected-resource
163
+ ```
164
+
165
+ ```json
166
+ {
167
+ "resource": "https://app.example.com/mcp/support",
168
+ "authorization_servers": ["https://auth.example.com"],
169
+ "scopes_supported": ["mcp:tools"],
170
+ "bearer_methods_supported": ["header"],
171
+ "resource_name": "support"
172
+ }
173
+ ```
174
+
175
+ Responses that reject a token point clients to it:
176
+
177
+ | Case | Status | `WWW-Authenticate` |
178
+ |---|---|---|
179
+ | No token | `401` | `Bearer realm="support", resource_metadata="https://app.example.com/mcp/support/.well-known/oauth-protected-resource", scope="mcp:tools"` |
180
+ | Invalid, expired or wrong-audience token | `401` | Same, plus `error="invalid_token"` |
181
+ | Missing scope | `403` | Same, plus `error="insufficient_scope"` |
182
+
183
+ Tokens are only read from the `Authorization` header, never from the query string.
184
+
185
+ ## No Authentication
186
+
187
+ ```ruby
188
+ authenticate :none
189
+ ```
190
+
191
+ Every request is accepted and the principal is `nil`. Use this only for servers that are not reachable from outside your machine or network, or for tools that are safe for anyone to run.
192
+
193
+ ## stdio
194
+
195
+ The stdio transport runs as a local subprocess that the MCP client starts. It already has your shell's access, so it does **not** run `authenticate`. Use `stdio_principal` to choose who the local caller is:
196
+
197
+ ```ruby
198
+ stdio_principal { User.find_by(email: ENV["MCP_USER_EMAIL"]) }
199
+ ```
200
+
201
+ The block runs once when the process starts. Without it, the principal is `nil`. `authorize_tool` and `context` still apply over stdio.
202
+
203
+ ## Origin Checks
204
+
205
+ Browsers send an `Origin` header. A malicious web page can use DNS rebinding to make a browser send requests to a server on `localhost`. For this reason, the MCP specification requires servers to check `Origin`. OmniAgent rejects requests that have an `Origin` header that is not in the allowlist with `403 Forbidden`. Requests without an `Origin` header are allowed. CLI and desktop clients do not send one.
206
+
207
+ If a browser-based client must call your server, allow its origin on the server or globally:
208
+
209
+ ```ruby
210
+ class SupportServer < OmniAgent::MCP::Server
211
+ allowed_origins "https://inspector.example.com", %r{\Ahttps://.*\.mycompany\.com\z}
212
+ end
213
+
214
+ OmniAgent.configure do |config|
215
+ config.mcp_allowed_origins = [ "https://inspector.example.com" ]
216
+ end
217
+ ```
218
+
219
+ ## Security Checklist
220
+
221
+ * Serve the endpoint over **HTTPS** in production. Bearer tokens in plain HTTP can be read by anyone on the network.
222
+ * Give each client its own token (Hash form) so you can revoke one without affecting the others.
223
+ * Hide destructive or expensive tools (such as [exposed agents](defining-servers.md#exposing-agents)) with `authorize_tool`.
224
+ * Tool error messages are returned to the client. Do not put secrets in exception messages.
225
+ * Request bodies larger than `mcp_max_request_bytes` (default 1 MiB) are rejected with `413`. See [Configuration](../configuration.md).
226
+ * With [OAuth 2.1](#oauth-21), set `resource:` to the exact public URL of the endpoint, and keep `scopes:` minimal.
@@ -0,0 +1,196 @@
1
+ <!-- Generated from omniagent-docs/docs/mcp/defining-servers.mdx by `bundle exec rake docs:sync`. Do not edit by hand. -->
2
+ <!-- Online version: https://omniagent.activetaskers.com/docs/mcp/defining-servers -->
3
+
4
+ # Defining MCP Servers
5
+
6
+ The [Model Context Protocol](https://modelcontextprotocol.io) (MCP) lets AI clients such as Claude Code, Claude Desktop and Cursor call tools that live in your app. `OmniAgent::MCP::Server` turns the `OmniAgent::Tool` classes you already wrote for your agents — and the agents themselves — into an MCP server, with no extra gem.
7
+
8
+ v1 supports MCP **tools** only (no resources or prompts), over [Streamable HTTP and stdio](transports.md).
9
+
10
+ A server gets its tools in two ways, and you can use both together:
11
+
12
+ * **Its own `tools/` directory**: tools that only this server uses. They are added automatically, like agent tools.
13
+ * **The DSL**: `tools`, `tool`, `tools_from` and `expose_agent` reuse tools and agents that you already have.
14
+
15
+ ## Generate a Server
16
+
17
+ ```bash
18
+ rails generate omni_agent:mcp_server Support --agents SupportAgent
19
+ ```
20
+
21
+ This creates `app/mcp_servers/support_server.rb` and mounts it at `/mcp/support`. Add `--with-tools LookupOrder RefundOrder` to also scaffold server tools. See [Generators](../generators.md#mcp-server-generator) for every option.
22
+
23
+ ## The Server Tools Directory
24
+
25
+ Put tools that belong only to the server in `app/mcp_servers/<server_name>/tools/`, namespaced under `<ServerClass>::Tools`:
26
+
27
+ ```text
28
+ app/mcp_servers/
29
+ ├── support_server.rb
30
+ └── support_server/
31
+ └── tools/
32
+ ├── lookup_order.rb
33
+ └── refund_order.rb
34
+ ```
35
+
36
+ ```ruby
37
+ # app/mcp_servers/support_server/tools/lookup_order.rb
38
+ module SupportServer::Tools
39
+ class LookupOrder < OmniAgent::Tool
40
+ description "Find an order by number."
41
+ annotations read_only: true
42
+
43
+ input do
44
+ string :number, description: "Order number"
45
+ end
46
+
47
+ def execute(number:)
48
+ Order.find_by!(number: number).as_json(only: %i[number status total])
49
+ end
50
+ end
51
+ end
52
+ ```
53
+
54
+ Every `OmniAgent::Tool` subclass in `SupportServer::Tools` is exposed under its class name (`LookupOrder`). There is nothing to register. Rails autoloads these files and reloads them in development, so new or changed tools appear on the next request.
55
+
56
+ Directory tools are listed first, sorted by name, followed by the tools you declare with the DSL. A server that inherits from another server also gets the parent's directory tools.
57
+
58
+ ## The Server DSL
59
+
60
+ ```ruby
61
+ # app/mcp_servers/support_server.rb
62
+ class SupportServer < OmniAgent::MCP::Server
63
+ server_name "support" # default: underscored class name minus _server
64
+ server_version "1.0.0" # default: "0.1.0"
65
+ instructions "Tools for the support team."
66
+
67
+ tools ResearchAgent::Tools::GetWeather, TaskAgent::Tools::SetPriority
68
+ tool TaskAgent::Tools::AssignTicket, as: "assign_ticket"
69
+ tools_from TaskAgent
70
+ expose_agent ResearchAgent, as: :research, description: "Ask the research agent", forward: [ :current_user ]
71
+
72
+ authenticate :bearer, tokens: -> { ENV.fetch("SUPPORT_MCP_TOKENS", "").split(",") }
73
+ authorize_tool { |tool_class, principal| principal.admin? || !tool_class.tags.include?(:admin) }
74
+ context { |principal, request| { current_user: principal } }
75
+ end
76
+ ```
77
+
78
+ * **`server_name`** / **`server_version`**: Reported to the client in `serverInfo` during the `initialize` handshake.
79
+ * **`instructions`**: Optional text the client can show to its model, describing how to use your tools.
80
+ * **`<ServerClass>::Tools`**: Added automatically. See [The Server Tools Directory](#the-server-tools-directory).
81
+ * **`tools`**: One or more `OmniAgent::Tool` classes. Each is exposed under its demodulized class name (`ResearchAgent::Tools::GetWeather` → `GetWeather`), the same name agents use.
82
+ * **`tool klass, as:`**: Expose a single tool under a different name.
83
+ * **`tools_from AgentClass`**: Expose every tool the agent has — its `<AgentClass>::Tools` namespace plus any [`delegate_to`](../agent/delegation.md) tools. Resolved on each request, so new tools show up without editing the server.
84
+ * **`expose_agent`**: Expose a whole agent as a single tool. See [Exposing Agents](#exposing-agents).
85
+ * **`authenticate`**: Required for HTTP. See [Authentication](authentication.md).
86
+ * **`authorize_tool`** / **`context`**: See [Per-Principal Tool Access](#per-principal-tool-access) and [Tool Context](#tool-context).
87
+
88
+ Tool classes can also be passed as strings (`tools "ResearchAgent::Tools::GetWeather"`). They are resolved when the tool list is built, which helps if the server file loads before your agents.
89
+
90
+ ### Tool Names
91
+
92
+ MCP tool names must be 1–128 characters from `A-Z a-z 0-9 _ - .`. Two tools with the same name raise `OmniAgent::MCPError`, including a directory tool and a declared tool. If two agents both define a `Search` tool, rename one:
93
+
94
+ ```ruby
95
+ tools_from DocsAgent
96
+ tool TicketsAgent::Tools::Search, as: "search_tickets"
97
+ ```
98
+
99
+ ### Sharing Configuration
100
+
101
+ Servers inherit from parent servers. Settings (`authenticate`, `authorize_tool`, `context`, `stdio_principal`, `instructions`, `server_version`) use the nearest declaration. Tools and allowed origins add up:
102
+
103
+ ```ruby
104
+ class ApplicationMcpServer < OmniAgent::MCP::Server
105
+ authenticate { |request| User.find_by(api_token: request.bearer_token) }
106
+ context { |principal, _request| { current_user: principal } }
107
+ end
108
+
109
+ class SupportServer < ApplicationMcpServer
110
+ tools_from SupportAgent
111
+ end
112
+ ```
113
+
114
+ ## Tool Annotations
115
+
116
+ MCP clients use optional hints to decide, for example, whether to ask the user before a call. Declare them on the tool class:
117
+
118
+ ```ruby
119
+ class GetWeather < OmniAgent::Tool
120
+ title "Get weather"
121
+ annotations read_only: true, destructive: false, idempotent: true, open_world: true
122
+ end
123
+ ```
124
+
125
+ | DSL key | MCP annotation | Meaning |
126
+ |---|---|---|
127
+ | `read_only:` | `readOnlyHint` | The tool does not change anything |
128
+ | `destructive:` | `destructiveHint` | The tool can delete or overwrite data |
129
+ | `idempotent:` | `idempotentHint` | Calling it again with the same arguments has no extra effect |
130
+ | `open_world:` | `openWorldHint` | The tool talks to external systems |
131
+
132
+ `title` is a human-friendly display name. Neither `title` nor `annotations` is sent to LLM providers when the tool is used by an agent. Clients treat annotations as hints, not as guarantees, so do not rely on them for security.
133
+
134
+ ## How Calls Work
135
+
136
+ * `tools/list` returns each tool's name, `title`, `description`, `inputSchema` (the same JSON schema built by `input do ... end`) and annotations.
137
+ * `tools/call` creates a new tool instance, sets its `context`, and calls `invoke` with the client's arguments. Arguments are validated exactly as they are for agents.
138
+ * **Return values**: a `String` becomes text content. A `Hash` becomes JSON text plus `structuredContent`. An `Array` becomes JSON text. `nil` becomes empty text. Any other value is converted with `to_s`.
139
+ * **Errors**: Validation failures (`ArgumentError`) and exceptions raised by `#execute` return a tool result with `isError: true` and the text `Error executing tool: <message>`, the same text agents receive. The model can then correct its arguments. Errors are logged to `Rails.logger`.
140
+ * An unknown tool name, or a tool the principal may not use, returns JSON-RPC error `-32602`.
141
+
142
+ ## Exposing Agents
143
+
144
+ `expose_agent` wraps an agent as one tool with a single `input` string argument. It works like [`delegate_to`](../agent/delegation.md): the agent runs its full loop, including its own tool calls, and returns its final answer as the tool result.
145
+
146
+ ```ruby
147
+ expose_agent ResearchAgent, as: :research,
148
+ description: "Ask the research agent",
149
+ run_alias: :summarize,
150
+ forward: [ :current_user ]
151
+ ```
152
+
153
+ * **`as:`**: The MCP tool name, used as given (`:research` → `research`).
154
+ * **`description:`**: Defaults to `"Delegate to <AgentClass>."`.
155
+ * **`run_alias:`**: Call a [run entrypoint](../agent/run-entrypoints.md) instead of `run`.
156
+ * **`forward:`**: Tool context keys passed to the agent's `context:`. Use `true` for all keys. The default is `[]` (no context). Combine with the [`context`](#tool-context) block to give the agent the current user.
157
+
158
+ Each call to an exposed agent is a call to your LLM provider, so it costs money. Protect these tools with authentication and, if needed, `authorize_tool`.
159
+
160
+ ## Per-Principal Tool Access
161
+
162
+ `authorize_tool` decides which tools each principal can see and call. It receives the tool class, the principal returned by [authentication](authentication.md) and the registered tool name. It runs for both `tools/list` and `tools/call`. A hidden tool is reported as unknown.
163
+
164
+ ```ruby
165
+ authorize_tool do |tool_class, principal, name|
166
+ next true if principal.admin?
167
+
168
+ !tool_class.tags.include?(:admin) && name != "research"
169
+ end
170
+ ```
171
+
172
+ The block runs on the server instance, so `transport` (`:http` or `:stdio`) and `request` are also available.
173
+
174
+ ## Tool Context
175
+
176
+ Every MCP tool call sets `tool.context` to:
177
+
178
+ ```ruby
179
+ { mcp_principal: principal, mcp_request: request } # request is nil over stdio
180
+ ```
181
+
182
+ With [`authenticate :oauth`](authentication.md#oauth-21), it also includes `mcp_access_token`, the validated `OmniAgent::MCP::OAuth::AccessToken`.
183
+
184
+ The `context` block merges extra keys. Use it to give tools that you share with agents the same keys that the agents give them:
185
+
186
+ ```ruby
187
+ context { |principal, request| { current_user: principal, ip: request&.ip } }
188
+ ```
189
+
190
+ ```ruby
191
+ class SetPriority < OmniAgent::Tool
192
+ def execute(ticket:, priority:)
193
+ Ticket.find(ticket).update!(priority: priority, updated_by: context[:current_user])
194
+ end
195
+ end
196
+ ```
@@ -0,0 +1,166 @@
1
+ <!-- Generated from omniagent-docs/docs/mcp/transports.mdx by `bundle exec rake docs:sync`. Do not edit by hand. -->
2
+ <!-- Online version: https://omniagent.activetaskers.com/docs/mcp/transports -->
3
+
4
+ # Transports and Clients
5
+
6
+ OmniAgent supports both MCP transports:
7
+
8
+ * **Streamable HTTP**: Mount the server in your routes with `mcp_server`. Remote clients connect to the URL.
9
+ * **stdio**: The client starts your app as a subprocess and talks to it over stdin and stdout. Use it for local development.
10
+
11
+ Both transports run the same server class.
12
+
13
+ ## Streamable HTTP
14
+
15
+ Mount servers in `config/routes.rb` with the `mcp_server` and `mcp_servers` route helpers:
16
+
17
+ ```ruby
18
+ Rails.application.routes.draw do
19
+ mcp_server :support # SupportServer at /mcp/support
20
+ mcp_server :billing, path: "/internal/mcp" # BillingServer at a custom path
21
+ mcp_server "admin/reports" # Admin::ReportsServer at /mcp/admin/reports
22
+ mcp_servers :inventory, :shipping # several servers at once
23
+ end
24
+ ```
25
+
26
+ The name is converted to the class name `<Name>Server`, and the default path is `/mcp/<name>`. Underscores in the path become dashes (`:customer_support` → `/mcp/customer-support`). Each route is named `<name>_mcp_server`, so you can use `support_mcp_server_url` to build the URL.
27
+
28
+ | Option | Default | Meaning |
29
+ |---|---|---|
30
+ | `path:` | `/mcp/<name>` | Mount path |
31
+ | `server:` | `"<Name>Server"` | Server class or class name, for servers that do not follow the naming convention |
32
+ | `as:` | `<name>_mcp_server` | Route name |
33
+
34
+ ### Mount Every Server
35
+
36
+ Call `mcp_servers` with no names to mount every server in `app/mcp_servers/` at its default path:
37
+
38
+ ```ruby
39
+ Rails.application.routes.draw do
40
+ mcp_servers
41
+ end
42
+ ```
43
+
44
+ * Every `*_server.rb` file is mounted, including files in subdirectories (`admin/reports_server.rb` → `/mcp/admin/reports`).
45
+ * Files inside a server's own directory, such as `support_server/tools/`, are skipped.
46
+ * Abstract base servers whose file name starts with `application_` (for example, `application_mcp_server.rb`) are skipped.
47
+ * Use `except:` to skip other servers. To give one server a custom path, skip it and mount it separately:
48
+
49
+ ```ruby
50
+ mcp_servers except: [ :billing ]
51
+ mcp_server :billing, path: "/internal/mcp"
52
+ ```
53
+
54
+ With this form, adding a server file adds an endpoint without a change to `routes.rb`. Every endpoint still requires [authentication](authentication.md), but review new server files with that in mind.
55
+
56
+ ### Lazy Loading
57
+
58
+ The helpers do not load your server classes. Each request looks up the class by name, so code reloading works in development and the routes file does not load your tools before your agents. `mcp_server` is shorthand for this lower-level form, which you can still use:
59
+
60
+ ```ruby
61
+ mount OmniAgent::MCP::RackApp.new("SupportServer") => "/mcp/support"
62
+ ```
63
+
64
+ The endpoint is **stateless**:
65
+
66
+ * Each `POST` carries one JSON-RPC message and gets one JSON response (`200`). Notifications get `202 Accepted` with no body.
67
+ * There are no sessions (`Mcp-Session-Id`) and no server-sent event stream. `GET` and `DELETE` return `405 Method Not Allowed`. The MCP specification allows both choices.
68
+ * Because there is no session state, the server works behind a load balancer with several Puma workers or hosts.
69
+ * Servers that use [`authenticate :oauth`](authentication.md#oauth-21) also answer `GET <endpoint>/.well-known/oauth-protected-resource` with their Protected Resource Metadata.
70
+
71
+ | Status | When |
72
+ |---|---|
73
+ | `200` | JSON-RPC response (including tool errors with `isError: true`) |
74
+ | `202` | Notification or client response accepted |
75
+ | `400` | Invalid JSON (`-32700`), invalid JSON-RPC message, or unsupported `MCP-Protocol-Version` header |
76
+ | `401` | Authentication failed |
77
+ | `403` | `Origin` header not allowed, or the OAuth token lacks a required scope (`error="insufficient_scope"`) |
78
+ | `405` | Method other than `POST` |
79
+ | `413` | Body larger than `mcp_max_request_bytes` |
80
+
81
+ Supported protocol versions are `2025-11-25`, `2025-06-18` and `2025-03-26`.
82
+
83
+ ### Try It With curl
84
+
85
+ ```bash
86
+ SUPPORT_MCP_TOKENS=secret bin/rails server
87
+
88
+ curl -s localhost:3000/mcp/support \
89
+ -H 'Content-Type: application/json' \
90
+ -H 'Authorization: Bearer secret' \
91
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
92
+ ```
93
+
94
+ ## stdio
95
+
96
+ Run the server from your Rails app root:
97
+
98
+ ```bash
99
+ bundle exec omni_agent mcp SupportServer
100
+ # or
101
+ bin/rails "omni_agent:mcp[SupportServer]"
102
+ ```
103
+
104
+ The process reads one JSON-RPC message per line from stdin and writes one response per line to stdout. Anything else written to stdout would corrupt the protocol, so OmniAgent redirects stdout to stderr after it boots. `puts` calls and loggers that write to stdout then go to stderr. The process exits when the client closes stdin.
105
+
106
+ stdio does not use `authenticate`. See [stdio authentication](authentication.md#stdio).
107
+
108
+ ## Connecting Clients
109
+
110
+ ### Claude Code
111
+
112
+ Over HTTP:
113
+
114
+ ```bash
115
+ claude mcp add --transport http support https://myapp.example.com/mcp/support \
116
+ --header "Authorization: Bearer $SUPPORT_MCP_TOKEN"
117
+ ```
118
+
119
+ Over stdio (run from your app root):
120
+
121
+ ```bash
122
+ claude mcp add support -- bundle exec omni_agent mcp SupportServer
123
+ ```
124
+
125
+ ### Claude Desktop
126
+
127
+ Add the server to `claude_desktop_config.json`. Desktop does not start the process in your app root, so change to it first:
128
+
129
+ ```json
130
+ {
131
+ "mcpServers": {
132
+ "support": {
133
+ "command": "bash",
134
+ "args": ["-lc", "cd /path/to/your/app && bundle exec omni_agent mcp SupportServer"]
135
+ }
136
+ }
137
+ }
138
+ ```
139
+
140
+ ### Cursor
141
+
142
+ Add the server to `.cursor/mcp.json`:
143
+
144
+ ```json
145
+ {
146
+ "mcpServers": {
147
+ "support": {
148
+ "url": "http://localhost:3000/mcp/support",
149
+ "headers": { "Authorization": "Bearer secret" }
150
+ }
151
+ }
152
+ }
153
+ ```
154
+
155
+ ### MCP Inspector
156
+
157
+ [MCP Inspector](https://github.com/modelcontextprotocol/inspector) is useful for debugging. It can list tools and call them by hand:
158
+
159
+ ```bash
160
+ npx @modelcontextprotocol/inspector --cli http://localhost:3000/mcp/support \
161
+ --transport http --header "Authorization: Bearer secret" --method tools/list
162
+
163
+ npx @modelcontextprotocol/inspector --cli bundle exec omni_agent mcp SupportServer --method tools/list
164
+ ```
165
+
166
+ The browser UI of the Inspector sends an `Origin` header. Add its origin to [`allowed_origins`](authentication.md#origin-checks) to use it over HTTP.