riffer 0.25.0 → 0.27.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/.agents/architecture.md +36 -2
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +19 -0
- data/README.md +1 -0
- data/docs/03_AGENTS.md +4 -0
- data/docs/10_CONFIGURATION.md +24 -5
- data/docs/13_SKILLS.md +1 -1
- data/docs/14_MCP.md +144 -0
- data/lib/riffer/agent.rb +81 -2
- data/lib/riffer/config.rb +13 -0
- data/lib/riffer/mcp/authenticated_tool.rb +65 -0
- data/lib/riffer/mcp/client.rb +67 -0
- data/lib/riffer/mcp/manifest.rb +45 -0
- data/lib/riffer/mcp/registration.rb +76 -0
- data/lib/riffer/mcp/registry.rb +62 -0
- data/lib/riffer/mcp/tool_factory.rb +54 -0
- data/lib/riffer/mcp.rb +57 -0
- data/lib/riffer/providers/amazon_bedrock.rb +18 -0
- data/lib/riffer/providers/anthropic.rb +43 -38
- data/lib/riffer/providers/base.rb +7 -3
- data/lib/riffer/providers/mock.rb +15 -0
- data/lib/riffer/providers/open_ai.rb +33 -26
- data/lib/riffer/version.rb +1 -1
- data/sig/generated/riffer/agent.rbs +33 -0
- data/sig/generated/riffer/config.rbs +20 -0
- data/sig/generated/riffer/mcp/authenticated_tool.rbs +17 -0
- data/sig/generated/riffer/mcp/client.rbs +33 -0
- data/sig/generated/riffer/mcp/manifest.rbs +30 -0
- data/sig/generated/riffer/mcp/registration.rbs +49 -0
- data/sig/generated/riffer/mcp/registry.rbs +35 -0
- data/sig/generated/riffer/mcp/tool_factory.rbs +25 -0
- data/sig/generated/riffer/mcp.rbs +51 -0
- data/sig/generated/riffer/providers/amazon_bedrock.rbs +15 -0
- data/sig/generated/riffer/providers/anthropic.rbs +4 -4
- data/sig/generated/riffer/providers/base.rbs +7 -3
- data/sig/generated/riffer/providers/mock.rbs +12 -0
- metadata +44 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7701c51a8dff2c1da03718cb852c86296ab738839163b86ddf46b5e4e78600da
|
|
4
|
+
data.tar.gz: f99baf1c378b7a8a8ce2ec8dad231cd01e70d25cee4a277c2d724c0377dfc56d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 423051f85e8fb037526b5aa6500b57b86fa4106a8a701e673df868c216661245a31702d9fb1fb3602a249d0f57dbe074a439e4f5f7a121d6d0c1c945d771f42f
|
|
7
|
+
data.tar.gz: a52e8baf66cebd4dc522dc1d836325f35442272d3fb02146c153156cd1232f08ada5cc3dc968033badd65df599fd453fa882d479afecddac8512e3c7d5e12bad
|
data/.agents/architecture.md
CHANGED
|
@@ -40,7 +40,7 @@ Adapters for LLM APIs. The base class uses a template-method pattern — `genera
|
|
|
40
40
|
|
|
41
41
|
Providers are registered in `Riffer::Providers::Repository::REPO` with identifiers (e.g., `openai`, `amazon_bedrock`).
|
|
42
42
|
|
|
43
|
-
Each provider declares a preferred skill adapter via `self.skills_adapter
|
|
43
|
+
Each provider declares a preferred skill adapter via `self.skills_adapter(model = nil)`. Default is Markdown; Anthropic returns XML; Amazon Bedrock returns XML when the model identifier matches an Anthropic model (e.g. `anthropic.claude-…` or `us.anthropic.claude-…`); Mock returns XML when the model name contains `claude`. The agent passes the resolved model identifier so proxy providers (Bedrock, Mock) can pick the right adapter without per-agent overrides.
|
|
44
44
|
|
|
45
45
|
### Skills (`lib/riffer/skills/`)
|
|
46
46
|
|
|
@@ -132,9 +132,35 @@ Built-in runtimes:
|
|
|
132
132
|
|
|
133
133
|
Context flow: `Agent#execute_tool_calls` → `ToolRuntime#execute(tool_calls, tools:, context:)` → `Runner#map(tool_calls, context:) { dispatch }` → `Tool#call(context:, **args)`
|
|
134
134
|
|
|
135
|
+
### MCP Integration (`lib/riffer/mcp/`)
|
|
136
|
+
|
|
137
|
+
Register third-party MCP servers globally; agents opt-in by tag via `use_mcp`. Tags are application-defined (manifests may list several; any overlap with `use_mcp` opts in—see `docs/14_MCP.md`).
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
Riffer::Mcp.register(
|
|
141
|
+
name: "github",
|
|
142
|
+
tags: [:github],
|
|
143
|
+
endpoint: "https://mcp.github.com",
|
|
144
|
+
discovery_headers: -> { {"Authorization" => "Bearer #{ENV['GITHUB_TOKEN']}"} }
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Optional: per-run tools/call headers (see docs/14_MCP.md)
|
|
148
|
+
Riffer.configure { |c| c.mcp.credentials = ->(manifest:, matched_tags:, context:) { ... } }
|
|
149
|
+
|
|
150
|
+
class ResearchAgent < Riffer::Agent
|
|
151
|
+
model "openai/gpt-5-mini"
|
|
152
|
+
use_mcp :github # picks up any :github-tagged registration
|
|
153
|
+
use_mcp :search, on_pending: :wait # per-call override
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Key types: `Manifest` (`discovery_headers`, optional `credentials_scope` hint), `Registry` (thread-safe store), `Registration` (spawns discovery thread → `ToolFactory`), `Client` (wraps `mcp` gem), `AuthenticatedTool` (wraps MCP tools when `credentials` proc is set).
|
|
158
|
+
|
|
159
|
+
**on_pending strategies** (global default `:ignore`): `:ignore` skips the server; `:wait` blocks until ready, re-raises failed discovery immediately, or times out; `:raise` re-raises failed discovery or `NotReadyError` while still pending.
|
|
160
|
+
|
|
135
161
|
## Key Patterns
|
|
136
162
|
|
|
137
|
-
- Model config accepts a `provider/model` string (e.g., `openai/gpt-
|
|
163
|
+
- Model config accepts a `provider/model` string (e.g., `openai/gpt-5-mini`) or a Proc/lambda that returns one
|
|
138
164
|
- Configuration via `Riffer.configure { |c| c.openai.api_key = "..." }`
|
|
139
165
|
- Providers use `depends_on` helper for runtime dependency checking
|
|
140
166
|
- Zeitwerk for autoloading - file structure must match module/class names
|
|
@@ -199,6 +225,14 @@ lib/
|
|
|
199
225
|
reasoning_done.rb # Reasoning done event
|
|
200
226
|
web_search_status.rb # Web search status event
|
|
201
227
|
web_search_done.rb # Web search done event
|
|
228
|
+
mcp.rb # MCP public API + error classes
|
|
229
|
+
mcp/
|
|
230
|
+
manifest.rb # Server config value object
|
|
231
|
+
registry.rb # Thread-safe global store
|
|
232
|
+
registration.rb # Per-server state + discovery thread
|
|
233
|
+
client.rb # Thin mcp gem wrapper
|
|
234
|
+
authenticated_tool.rb # Wraps MCP tools when credentials proc is configured
|
|
235
|
+
tool_factory.rb # Generates Riffer::Tool subclasses from MCP tools
|
|
202
236
|
test/
|
|
203
237
|
test_helper.rb # Minitest configuration with VCR
|
|
204
238
|
riffer_test.rb # Main module tests
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.27.0](https://github.com/janeapp/riffer/compare/riffer/v0.26.0...riffer/v0.27.0) (2026-05-01)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* integrate MCP server tools into agent tool resolution ([6c7a09a](https://github.com/janeapp/riffer/commit/6c7a09a1797dcc8fbafd198665fb665ede93f009))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* close streaming HTTP connections when consumer raises ([#235](https://github.com/janeapp/riffer/issues/235)) ([2a51a10](https://github.com/janeapp/riffer/commit/2a51a106d0f9d1410040c6b3a887722378a0dc14))
|
|
19
|
+
|
|
20
|
+
## [0.26.0](https://github.com/janeapp/riffer/compare/riffer/v0.25.0...riffer/v0.26.0) (2026-04-29)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* model-aware skills adapter selection ([#232](https://github.com/janeapp/riffer/issues/232)) ([74a5323](https://github.com/janeapp/riffer/commit/74a5323945f3f6f30538d472400cc5fe27b588da))
|
|
26
|
+
|
|
8
27
|
## [0.25.0](https://github.com/janeapp/riffer/compare/riffer/v0.24.2...riffer/v0.25.0) (2026-04-29)
|
|
9
28
|
|
|
10
29
|
|
data/README.md
CHANGED
|
@@ -60,6 +60,7 @@ For comprehensive documentation, see the [docs](docs/) directory:
|
|
|
60
60
|
- [Evals](docs/11_EVALS.md) - Evaluating agent quality
|
|
61
61
|
- [Guardrails](docs/12_GUARDRAILS.md) - Input/output validation
|
|
62
62
|
- [Skills](docs/13_SKILLS.md) - Packaged agent capabilities
|
|
63
|
+
- [MCP](docs/14_MCP.md) - Integrating third-party MCP servers
|
|
63
64
|
- [Providers](docs/providers/01_PROVIDERS.md) - LLM provider adapters
|
|
64
65
|
|
|
65
66
|
### API Reference
|
data/docs/03_AGENTS.md
CHANGED
|
@@ -124,6 +124,10 @@ class MyAgent < Riffer::Agent
|
|
|
124
124
|
end
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
+
### use_mcp
|
|
128
|
+
|
|
129
|
+
Loads tools from registered [MCP](14_MCP.md) servers by tag. Like `uses_tools`, **`use_mcp` is not inherited**—add it on each subclass that should include MCP tools.
|
|
130
|
+
|
|
127
131
|
### provider_options
|
|
128
132
|
|
|
129
133
|
Passes options to the provider client:
|
data/docs/10_CONFIGURATION.md
CHANGED
|
@@ -34,6 +34,25 @@ Riffer.config.anthropic.api_key
|
|
|
34
34
|
|
|
35
35
|
For provider credentials and setup, see the individual [Provider guides](providers/).
|
|
36
36
|
|
|
37
|
+
### MCP (Model Context Protocol)
|
|
38
|
+
|
|
39
|
+
Optional settings for [MCP server integrations](14_MCP.md):
|
|
40
|
+
|
|
41
|
+
| Option | Description |
|
|
42
|
+
| ------------------ | --------------------------------------------------------------------------------------------------------------- |
|
|
43
|
+
| `credentials` | Optional `Proc` for per-run `tools/call` HTTP headers: `->(manifest:, matched_tags:, context:) { Hash or nil }` |
|
|
44
|
+
| `discovery_runner` | `Riffer::Runner` instance for tool discovery (default `Runner::Sequential.new`) |
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
Riffer.configure do |config|
|
|
48
|
+
config.mcp.credentials = lambda do |manifest:, matched_tags:, context:|
|
|
49
|
+
{"Authorization" => "Bearer #{token_for(context)}"}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
See [MCP](14_MCP.md) for registration, tags, and agent `use_mcp`.
|
|
55
|
+
|
|
37
56
|
### Tool Runtime (Experimental)
|
|
38
57
|
|
|
39
58
|
> **Warning:** This feature is experimental and may be removed or changed without warning in a future release.
|
|
@@ -46,11 +65,11 @@ Riffer.configure do |config|
|
|
|
46
65
|
end
|
|
47
66
|
```
|
|
48
67
|
|
|
49
|
-
| Value
|
|
50
|
-
|
|
|
51
|
-
| `Riffer::ToolRuntime` subclass
|
|
52
|
-
| `Riffer::ToolRuntime` instance
|
|
53
|
-
| `Proc`
|
|
68
|
+
| Value | Description |
|
|
69
|
+
| ------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
70
|
+
| `Riffer::ToolRuntime` subclass | Instantiated automatically (e.g., `Riffer::ToolRuntime::Inline`, `Riffer::ToolRuntime::Threaded`) |
|
|
71
|
+
| `Riffer::ToolRuntime` instance | Custom runtime with specific options |
|
|
72
|
+
| `Proc` | Dynamic resolution |
|
|
54
73
|
|
|
55
74
|
Per-agent configuration overrides this global default. See [Advanced Tool Configuration — Tool Runtime](07_TOOL_ADVANCED.md#tool-runtime-experimental) for details.
|
|
56
75
|
|
data/docs/13_SKILLS.md
CHANGED
|
@@ -70,7 +70,7 @@ end
|
|
|
70
70
|
|
|
71
71
|
### Custom Adapter
|
|
72
72
|
|
|
73
|
-
The adapter controls how the skill catalog is rendered in the system prompt and which tool the LLM calls to activate a skill. The adapter is auto-selected by provider
|
|
73
|
+
The adapter controls how the skill catalog is rendered in the system prompt and which tool the LLM calls to activate a skill. The adapter is auto-selected by provider, with model-aware fallback for proxy providers — Markdown for most providers, XML for Anthropic, and XML for Anthropic models routed through Amazon Bedrock (e.g. `us.anthropic.claude-sonnet-4-6`). Override with:
|
|
74
74
|
|
|
75
75
|
```ruby
|
|
76
76
|
skills do
|
data/docs/14_MCP.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# MCP
|
|
2
|
+
|
|
3
|
+
Riffer can consume third-party [Model Context Protocol](https://modelcontextprotocol.io) (MCP) servers as tool sources. Tools are discovered automatically at registration time — no handwritten Ruby per tool required.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
1. Register an MCP server globally with `Riffer::Mcp.register`.
|
|
8
|
+
2. Opt an agent into that server's tools using the `use_mcp` DSL.
|
|
9
|
+
3. The agent picks up the tools and calls them like any other Riffer tool.
|
|
10
|
+
|
|
11
|
+
## Registering a Server
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
Riffer::Mcp.register(
|
|
15
|
+
name: "github", # unique identifier
|
|
16
|
+
tags: [:github], # agents opt-in by tag
|
|
17
|
+
endpoint: "https://mcp.github.com",
|
|
18
|
+
discovery_headers: -> { {"Authorization" => "Bearer #{ENV['GITHUB_TOKEN']}"} }
|
|
19
|
+
)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`register` blocks until tool discovery completes (or fails). Discovery uses the configured `Riffer::Runner` (default `Runner::Sequential` — inline). To run discovery on a pool thread (e.g. for Rails connection-pool isolation), set `config.mcp.discovery_runner = Riffer::Runner::Threaded.new`; `register` still blocks but the work runs off the calling thread.
|
|
23
|
+
|
|
24
|
+
### Discovery headers
|
|
25
|
+
|
|
26
|
+
`discovery_headers` is a Hash or Proc used **only** for MCP `tools/list` when the discovery client is built (the Proc runs once at that time). Use it for bootstrap identity: service account, env-based token, or `{}` if listing is unauthenticated.
|
|
27
|
+
|
|
28
|
+
Optional **`credentials_scope`** on the manifest documents whether you expect invocation headers to depend on tenant and/or user keys in the agent `context` (e.g. `:global`, `:tenant`, `:user`). It does **not** store tenant or user ids — only a hint for your app and docs. For multi-tenant apps, `:user` often means “user in tenant” and your `context` may include both tenant and user identifiers.
|
|
29
|
+
|
|
30
|
+
## Session credentials callback
|
|
31
|
+
|
|
32
|
+
When **`Riffer.config.mcp.credentials`** is set to a Proc, each MCP `tools/call` resolves HTTP headers through that callback instead of reusing `discovery_headers`.
|
|
33
|
+
|
|
34
|
+
**Signature:**
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
Riffer.configure do |config|
|
|
38
|
+
config.mcp.credentials = lambda do |manifest:, matched_tags:, context:|
|
|
39
|
+
# return nil to omit this server's tools for this agent run (at resolve time)
|
|
40
|
+
# return Hash<String,String> headers for tools/call (e.g. Authorization)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- **`manifest`** — the server's `Riffer::Mcp::Manifest`.
|
|
46
|
+
- **`matched_tags`** — intersection of the agent's `use_mcp` tags and `manifest.tags` for this registration (unioned across multiple `use_mcp` lines).
|
|
47
|
+
- **`context`** — the same hash passed to `generate` / `stream` for this run.
|
|
48
|
+
|
|
49
|
+
**Resolve time:** Before tools are exposed to the model, the proc is invoked once per matching registration. If it returns **`nil`**, that server's tools are omitted for this run (e.g. tenant has no integration).
|
|
50
|
+
|
|
51
|
+
**Call time:** Authenticated tool wrappers invoke the proc again for each execution. If it returns **`nil`**, `Riffer::Mcp::CredentialsDeniedError` is raised.
|
|
52
|
+
|
|
53
|
+
If **`credentials` is unset**, discovery and `tools/call` share one client built from `discovery_headers` (same behaviour as a single static token for both list and call).
|
|
54
|
+
|
|
55
|
+
## Tags
|
|
56
|
+
|
|
57
|
+
Tags are entirely up to your application; Riffer does not define a canonical vocabulary. Each server may declare multiple tags; an agent includes every registration that shares **any** tag passed to `use_mcp`.
|
|
58
|
+
|
|
59
|
+
When MCPs are registered, assign **stable bucket tags** so agent classes can opt in without listing every server name—for example `tags: [:connectors, :github]` with `use_mcp :connectors` for all enabled connectors, or `use_mcp :github` for that integration only.
|
|
60
|
+
|
|
61
|
+
Registrations are **global** (endpoint + tags); **tenant and user** access are enforced in your **`credentials`** proc and whatever you put in **`context`**, not by putting ids on the manifest.
|
|
62
|
+
|
|
63
|
+
## Opting an Agent In
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
class ResearchAgent < Riffer::Agent
|
|
67
|
+
model "openai/gpt-5-mini"
|
|
68
|
+
instructions "You are a research assistant."
|
|
69
|
+
|
|
70
|
+
use_mcp :github
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`use_mcp` accepts any tag registered via `Riffer::Mcp.register`. Multiple calls accumulate — the agent receives tools from all matching servers:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
class MultiAgent < Riffer::Agent
|
|
78
|
+
model "openai/gpt-5-mini"
|
|
79
|
+
|
|
80
|
+
use_mcp :github
|
|
81
|
+
use_mcp :jira
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
MCP tools are appended after any tools declared with `uses_tools`.
|
|
86
|
+
|
|
87
|
+
Tool names must be unique across `uses_tools` and all included MCP servers; duplicate names raise `Riffer::ArgumentError` when tools are resolved.
|
|
88
|
+
|
|
89
|
+
### Subclassing
|
|
90
|
+
|
|
91
|
+
Like [`uses_tools`](03_AGENTS.md#uses_tools), **`use_mcp` is not inherited** from the superclass. Declare `use_mcp` on each agent class that should load MCP tools.
|
|
92
|
+
|
|
93
|
+
## Unregistering a Server
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
Riffer::Mcp.unregister("github")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Subsequent agent runs will not include tools from that server. Call `register` again (with fresh `discovery_headers` if needed) to re-register.
|
|
100
|
+
|
|
101
|
+
## Introspection
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
Riffer::Mcp.registrations
|
|
105
|
+
# => {"github" => #<Riffer::Mcp::Registration ...>, ...}
|
|
106
|
+
|
|
107
|
+
reg = Riffer::Mcp.registrations["github"]
|
|
108
|
+
reg.tools # => [<Class:...>, ...] (Riffer::Tool subclasses)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Discovery failures raise from `register` directly, typically `Faraday::Error` for network issues or `Riffer::DependencyError` if the `mcp`/`faraday` gems are missing. Rescue `StandardError` for graceful degradation:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
begin
|
|
115
|
+
Riffer::Mcp.register(name: "github", ...)
|
|
116
|
+
rescue StandardError => e
|
|
117
|
+
Rails.logger.warn("MCP registration failed: #{e.message}")
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Error Classes
|
|
122
|
+
|
|
123
|
+
| Class | Raised when |
|
|
124
|
+
| ------------------------------------- | ---------------------------------------------------- |
|
|
125
|
+
| `Riffer::Mcp::CredentialsDeniedError` | `credentials` proc returns `nil` during `tools/call` |
|
|
126
|
+
|
|
127
|
+
All inherit from `Riffer::Mcp::Error < Riffer::Error`.
|
|
128
|
+
|
|
129
|
+
## Limitations
|
|
130
|
+
|
|
131
|
+
- **Tool results:** `tools/call` responses are reduced to joined **text** content from MCP `content` items. Non-text parts (e.g. images, embedded resources) are not surfaced in this release.
|
|
132
|
+
- **Session credentials:** When `Riffer.config.mcp.credentials` is set, authenticated tool wrappers may build a **new HTTP client per tool invocation** so headers stay fresh; there is no connection pooling in this release.
|
|
133
|
+
- **Context window / progressive disclosure:** Discovery registers **all** tools from each server; matching agents see the full set in one shot. There is no built-in lazy listing or prompt-size budgeting yet. Tighter control may require application-level filtering, MCP server design, or future Riffer APIs.
|
|
134
|
+
|
|
135
|
+
## Requirements
|
|
136
|
+
|
|
137
|
+
The `mcp` and `faraday` gems are **optional** dependencies — they are not included in Riffer's runtime gemspec. To use MCP integration, add both to your own Gemfile:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
gem "mcp"
|
|
141
|
+
gem "faraday"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Faraday is required for the MCP HTTP transport. If either gem is missing when `Riffer::Mcp.register` is called, a `Riffer::DependencyError` is raised.
|
data/lib/riffer/agent.rb
CHANGED
|
@@ -172,6 +172,24 @@ class Riffer::Agent
|
|
|
172
172
|
end
|
|
173
173
|
private_class_method :resolve_uses_tools_config
|
|
174
174
|
|
|
175
|
+
# Opts this agent into tools from all MCP registrations that share any of
|
|
176
|
+
# the given tag(s).
|
|
177
|
+
#
|
|
178
|
+
# +tag+ - a String or Symbol; matched against registration manifest tags.
|
|
179
|
+
#
|
|
180
|
+
#: (String | Symbol) -> void
|
|
181
|
+
def self.use_mcp(tag)
|
|
182
|
+
@mcp_configs ||= []
|
|
183
|
+
@mcp_configs << {tags: [tag.to_sym]}
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Returns the accumulated +use_mcp+ configurations for this agent class.
|
|
187
|
+
#
|
|
188
|
+
#: () -> Array[Hash[Symbol, untyped]]
|
|
189
|
+
def self.mcp_configs
|
|
190
|
+
@mcp_configs || []
|
|
191
|
+
end
|
|
192
|
+
|
|
175
193
|
# Gets or sets the tool runtime for this agent.
|
|
176
194
|
#
|
|
177
195
|
# Accepts a Riffer::ToolRuntime subclass, a Riffer::ToolRuntime instance,
|
|
@@ -755,9 +773,70 @@ class Riffer::Agent
|
|
|
755
773
|
end
|
|
756
774
|
|
|
757
775
|
#--
|
|
776
|
+
#: () -> Array[singleton(Riffer::Tool)]
|
|
777
|
+
def resolve_uses_tools_config
|
|
778
|
+
config = self.class.uses_tools
|
|
779
|
+
|
|
780
|
+
if config.nil?
|
|
781
|
+
[]
|
|
782
|
+
elsif config.is_a?(Proc)
|
|
783
|
+
(config.arity == 0) ? config.call : config.call(@context)
|
|
784
|
+
else
|
|
785
|
+
config
|
|
786
|
+
end
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
#--
|
|
790
|
+
#: () -> Array[singleton(Riffer::Tool)]
|
|
791
|
+
def resolve_mcp_tool_classes
|
|
792
|
+
configs = self.class.mcp_configs
|
|
793
|
+
return [] if configs.empty?
|
|
794
|
+
|
|
795
|
+
cred = Riffer.config.mcp.credentials
|
|
796
|
+
ctx = @context
|
|
797
|
+
gather_mcp_registrations_with_tags(configs).flat_map do |reg, tag_accum|
|
|
798
|
+
matched_tags = tag_accum.uniq
|
|
799
|
+
mcp_tools_for_registration(reg, matched_tags, cred, ctx)
|
|
800
|
+
end
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
# Each matching MCP registration once, with tag symbols unioned across +use_mcp+ rows.
|
|
804
|
+
#
|
|
805
|
+
#: (Array[Hash[Symbol, untyped]]) -> Hash[Riffer::Mcp::Registration, Array[Symbol]]
|
|
806
|
+
def gather_mcp_registrations_with_tags(configs)
|
|
807
|
+
by_reg = {}
|
|
808
|
+
configs.each do |cfg|
|
|
809
|
+
Riffer::Mcp::Registry.find_by_tags(cfg[:tags]).each do |reg|
|
|
810
|
+
(by_reg[reg] ||= []).concat(cfg[:tags] & reg.manifest.tags)
|
|
811
|
+
end
|
|
812
|
+
end
|
|
813
|
+
by_reg
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
#: (Riffer::Mcp::Registration, Array[Symbol], Proc?, Hash[Symbol, untyped]) -> Array[singleton(Riffer::Tool)]
|
|
817
|
+
def mcp_tools_for_registration(reg, matched_tags, cred, ctx)
|
|
818
|
+
return reg.tools unless cred
|
|
819
|
+
return [] if cred.call(manifest: reg.manifest, matched_tags: matched_tags, context: ctx).nil?
|
|
820
|
+
Riffer::Mcp::AuthenticatedTool.wrap_all(reg.tools, reg.manifest, matched_tags)
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
# Raises if two or more tool classes share the same +.name+ (ambiguous dispatch).
|
|
824
|
+
#
|
|
825
|
+
#: (Array[singleton(Riffer::Tool)]) -> void
|
|
826
|
+
def assert_distinct_tool_names!(tool_classes)
|
|
827
|
+
tally = Hash.new(0) #: Hash[String, Integer]
|
|
828
|
+
tool_classes.each { |tc| tally[tc.name] += 1 }
|
|
829
|
+
dupes = tally.filter_map { |name, n| name if n > 1 }
|
|
830
|
+
return if dupes.empty?
|
|
831
|
+
|
|
832
|
+
raise Riffer::ArgumentError, "Duplicate tool names: #{dupes.sort.join(", ")}"
|
|
833
|
+
end
|
|
834
|
+
|
|
758
835
|
#: () -> Array[singleton(Riffer::Tool)]
|
|
759
836
|
def resolved_tools
|
|
760
|
-
@resolved_tools ||= self.class.resolved_tool_classes(context: @context)
|
|
837
|
+
@resolved_tools ||= self.class.resolved_tool_classes(context: @context) + resolve_mcp_tool_classes
|
|
838
|
+
assert_distinct_tool_names!(@resolved_tools)
|
|
839
|
+
@resolved_tools
|
|
761
840
|
end
|
|
762
841
|
|
|
763
842
|
#--
|
|
@@ -799,7 +878,7 @@ class Riffer::Agent
|
|
|
799
878
|
return nil if skills_list.empty?
|
|
800
879
|
|
|
801
880
|
skills = skills_list.to_h { |s| [s.name, s] }
|
|
802
|
-
adapter_class = self.class.skills.adapter || provider_class.skills_adapter
|
|
881
|
+
adapter_class = self.class.skills.adapter || provider_class.skills_adapter(@model_name)
|
|
803
882
|
skill_activate_tool_class = self.class.skills.activate_tool || Riffer.config.skills.default_activate_tool
|
|
804
883
|
|
|
805
884
|
skills_context = Riffer::Skills::Context.new(
|
data/lib/riffer/config.rb
CHANGED
|
@@ -21,6 +21,7 @@ class Riffer::Config
|
|
|
21
21
|
Gemini = Struct.new(:api_key, :open_timeout, :read_timeout, keyword_init: true)
|
|
22
22
|
OpenAI = Struct.new(:api_key, keyword_init: true)
|
|
23
23
|
Evals = Struct.new(:judge_model, keyword_init: true)
|
|
24
|
+
Mcp = Struct.new(:credentials, :discovery_runner, keyword_init: true)
|
|
24
25
|
|
|
25
26
|
# Skills-related global configuration.
|
|
26
27
|
#
|
|
@@ -93,6 +94,17 @@ class Riffer::Config
|
|
|
93
94
|
# Evals configuration (Struct with +judge_model+).
|
|
94
95
|
attr_reader :evals #: Riffer::Config::Evals
|
|
95
96
|
|
|
97
|
+
# MCP configuration (Struct with +credentials+ and +discovery_runner+).
|
|
98
|
+
#
|
|
99
|
+
# +credentials+ is an optional Proc for per-run MCP +tools/call+ HTTP headers.
|
|
100
|
+
# Signature: +->(manifest:, matched_tags:, context:) { Hash or nil }+.
|
|
101
|
+
# +nil+ from the proc at tool-resolution time omits that server's tools; +nil+
|
|
102
|
+
# at tool-call time raises Riffer::Mcp::CredentialsDeniedError.
|
|
103
|
+
#
|
|
104
|
+
# +discovery_runner+ is the Riffer::Runner used to execute tool discovery
|
|
105
|
+
# (default +Runner::Sequential+).
|
|
106
|
+
attr_reader :mcp #: Riffer::Config::Mcp
|
|
107
|
+
|
|
96
108
|
# Global tool runtime configuration (experimental).
|
|
97
109
|
#
|
|
98
110
|
# Accepts a Riffer::ToolRuntime subclass, a Riffer::ToolRuntime instance,
|
|
@@ -148,6 +160,7 @@ class Riffer::Config
|
|
|
148
160
|
@gemini = Gemini.new
|
|
149
161
|
@openai = OpenAI.new
|
|
150
162
|
@evals = Evals.new
|
|
163
|
+
@mcp = Mcp.new(credentials: nil, discovery_runner: Riffer::Runner::Sequential.new)
|
|
151
164
|
@tool_runtime = Riffer::ToolRuntime::Inline.new
|
|
152
165
|
@skills = Skills.new
|
|
153
166
|
@message_id_strategy = :none
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# Wraps MCP-generated tool classes so +tools/call+ uses +Riffer.config.mcp.credentials+
|
|
5
|
+
# per invocation while delegating metadata to the inner class.
|
|
6
|
+
#
|
|
7
|
+
module Riffer::Mcp::AuthenticatedTool
|
|
8
|
+
# Returns one wrapper class per inner tool, sharing +manifest+ and +matched_tags+.
|
|
9
|
+
#
|
|
10
|
+
#--
|
|
11
|
+
#: (Array[singleton(Riffer::Tool)], Riffer::Mcp::Manifest, Array[Symbol]) -> Array[singleton(Riffer::Tool)]
|
|
12
|
+
def self.wrap_all(tool_classes, manifest, matched_tags)
|
|
13
|
+
tool_classes.map { |tc| wrap_one(tc, manifest, matched_tags) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
#--
|
|
17
|
+
#: (singleton(Riffer::Tool), Riffer::Mcp::Manifest, Array[Symbol]) -> singleton(Riffer::Tool)
|
|
18
|
+
# Class.new(Riffer::Tool) is typed as ::Class by steep — it cannot verify the subtype
|
|
19
|
+
# relationship for dynamically created anonymous classes, so the ignore is required.
|
|
20
|
+
def self.wrap_one(inner_class, manifest, matched_tags) # steep:ignore MethodBodyTypeMismatch
|
|
21
|
+
inner = inner_class
|
|
22
|
+
man = manifest
|
|
23
|
+
tags = matched_tags
|
|
24
|
+
|
|
25
|
+
Class.new(Riffer::Tool) do
|
|
26
|
+
@identifier = inner.identifier
|
|
27
|
+
|
|
28
|
+
define_singleton_method(:name) { inner.name }
|
|
29
|
+
define_singleton_method(:mcp_server_tool_name) { inner.mcp_server_tool_name }
|
|
30
|
+
define_singleton_method(:description) { inner.description }
|
|
31
|
+
define_singleton_method(:parameters_schema) { |strict: false| inner.parameters_schema(strict: strict) }
|
|
32
|
+
|
|
33
|
+
# Builds a client for a single +tools/call+ invocation.
|
|
34
|
+
#
|
|
35
|
+
# Creates a fresh client per call so headers from the credentials proc stay
|
|
36
|
+
# current.
|
|
37
|
+
# TODO: A per-headers cache would reduce connection churn under load, and
|
|
38
|
+
# requires a follow-up investigation to determine how to invalidate failing
|
|
39
|
+
# clients.
|
|
40
|
+
define_method(:build_call_client) do |endpoint, headers|
|
|
41
|
+
Riffer::Mcp::Client.new(endpoint: endpoint, headers: headers)
|
|
42
|
+
end
|
|
43
|
+
private :build_call_client
|
|
44
|
+
|
|
45
|
+
define_method(:call) do |context:, **kwargs|
|
|
46
|
+
cred = Riffer.config.mcp.credentials
|
|
47
|
+
unless cred
|
|
48
|
+
# `next` rather than `return`: inside define_method the block IS the method
|
|
49
|
+
# body, so both exit :call identically at runtime. `next` avoids a false
|
|
50
|
+
# steep ReturnTypeMismatch that would otherwise need a steep:ignore.
|
|
51
|
+
next inner.new.call(context: context, **kwargs)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
headers = cred.call(manifest: man, matched_tags: tags, context: context)
|
|
55
|
+
if headers.nil?
|
|
56
|
+
raise Riffer::Mcp::CredentialsDeniedError,
|
|
57
|
+
"MCP credentials returned nil for server '#{man.name}' during tools/call"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
client = build_call_client(man.endpoint, headers)
|
|
61
|
+
text(client.tools_call(inner.mcp_server_tool_name, kwargs))
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# Thin wrapper around the MCP Ruby SDK client (mcp gem v0.8+).
|
|
5
|
+
#
|
|
6
|
+
# Resolves headers (if a Proc) once at initialization, then provides
|
|
7
|
+
# +tools_list+ and +tools_call+. Used for discovery (+Manifest#discovery_headers+)
|
|
8
|
+
# and for +tools/call+ when no +credentials+ proc is configured.
|
|
9
|
+
#
|
|
10
|
+
# MCP gem API used:
|
|
11
|
+
# MCP::Client::HTTP.new(url:, headers:) — HTTP transport (requires faraday)
|
|
12
|
+
# MCP::Client.new(transport:) — client
|
|
13
|
+
# client.tools — Array<MCP::Client::Tool>
|
|
14
|
+
# client.call_tool(tool:, arguments:) — raw JSON-RPC response Hash
|
|
15
|
+
#
|
|
16
|
+
class Riffer::Mcp::Client
|
|
17
|
+
include Riffer::Helpers::Dependencies
|
|
18
|
+
|
|
19
|
+
#--
|
|
20
|
+
#: (endpoint: String, ?headers: (Hash[String, String] | Proc), ?client: untyped?) -> void
|
|
21
|
+
def initialize(endpoint:, headers: {}, client: nil)
|
|
22
|
+
depends_on "mcp"
|
|
23
|
+
depends_on "faraday"
|
|
24
|
+
|
|
25
|
+
@client = client || begin
|
|
26
|
+
resolved_headers = headers.is_a?(Proc) ? headers.call : headers
|
|
27
|
+
transport = MCP::Client::HTTP.new(url: endpoint, headers: resolved_headers)
|
|
28
|
+
MCP::Client.new(transport: transport)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns an array of tool definition hashes, each with +:name+, +:description+,
|
|
33
|
+
# and +:input_schema+ keys.
|
|
34
|
+
#
|
|
35
|
+
#--
|
|
36
|
+
#: () -> Array[Hash[Symbol, untyped]]
|
|
37
|
+
def tools_list
|
|
38
|
+
@client.tools.map do |tool|
|
|
39
|
+
{
|
|
40
|
+
name: tool.name,
|
|
41
|
+
description: tool.description,
|
|
42
|
+
input_schema: tool.input_schema
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Calls a tool on the MCP server and returns joined text content from the response.
|
|
48
|
+
#
|
|
49
|
+
#--
|
|
50
|
+
#: (String, ?Hash[untyped, untyped]) -> String
|
|
51
|
+
def tools_call(name, arguments = {})
|
|
52
|
+
tool = MCP::Client::Tool.new(name: name, description: nil, input_schema: nil)
|
|
53
|
+
response = @client.call_tool(tool: tool, arguments: arguments)
|
|
54
|
+
|
|
55
|
+
if response["error"]
|
|
56
|
+
raise Riffer::Error, response.dig("error", "message") || "MCP tool call failed"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if response.dig("result", "isError")
|
|
60
|
+
message = (response.dig("result", "content") || []).filter_map { |item| item["text"] }.join
|
|
61
|
+
raise Riffer::Error, message.empty? ? "MCP tool '#{name}' failed" : message
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
content = response.dig("result", "content") || []
|
|
65
|
+
content.filter_map { |item| item["text"] }.join
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
# Riffer::Mcp::Manifest holds the configuration for a single MCP server.
|
|
7
|
+
#
|
|
8
|
+
# +name+ - String identifier used as the registration key and generated-agent identifier.
|
|
9
|
+
# +tags+ - Array[Symbol]; normalized to symbols at construction time.
|
|
10
|
+
# +endpoint+ - String HTTPS URL passed to the MCP transport.
|
|
11
|
+
# +discovery_headers+ - Hash or Proc; resolved once when building the discovery client for +tools/list+.
|
|
12
|
+
# +credentials_scope+ - Optional symbol hint: +:global+, +:tenant+, +:user+ — documents whether
|
|
13
|
+
# invocation credentials are expected to depend on tenant and/or user keys in +context+ (no ids stored).
|
|
14
|
+
# Apps may treat +:user+ as "user in tenant" and pass both keys in +context+.
|
|
15
|
+
#
|
|
16
|
+
class Riffer::Mcp::Manifest
|
|
17
|
+
attr_reader :name #: String
|
|
18
|
+
attr_reader :tags #: Array[Symbol]
|
|
19
|
+
attr_reader :endpoint #: String
|
|
20
|
+
attr_reader :discovery_headers #: (Hash[String, untyped] | ::Proc)?
|
|
21
|
+
attr_reader :credentials_scope #: Symbol?
|
|
22
|
+
|
|
23
|
+
#--
|
|
24
|
+
#: (name: String, endpoint: String, ?tags: Array[untyped]?, ?discovery_headers: (Hash[String, untyped] | ::Proc)?, ?credentials_scope: (String | Symbol)?) -> void
|
|
25
|
+
def initialize(name:, endpoint:, tags: nil, discovery_headers: nil, credentials_scope: nil)
|
|
26
|
+
@name = name.to_s.strip
|
|
27
|
+
raise Riffer::ArgumentError, "MCP manifest name is required" if @name.empty?
|
|
28
|
+
|
|
29
|
+
@endpoint = endpoint.to_s.strip
|
|
30
|
+
raise Riffer::ArgumentError, "MCP manifest endpoint must be a valid HTTPS URL" unless valid_endpoint?
|
|
31
|
+
|
|
32
|
+
@tags = Array(tags).map(&:to_sym)
|
|
33
|
+
@discovery_headers = discovery_headers
|
|
34
|
+
@credentials_scope = credentials_scope&.to_sym
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def valid_endpoint?
|
|
40
|
+
uri = URI.parse(@endpoint)
|
|
41
|
+
uri.is_a?(URI::HTTPS) && uri.host
|
|
42
|
+
rescue URI::InvalidURIError
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
end
|