agentf 0.3.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 +7 -0
- data/bin/agentf +8 -0
- data/lib/agentf/agent_policy.rb +54 -0
- data/lib/agentf/agents/architect.rb +67 -0
- data/lib/agentf/agents/base.rb +53 -0
- data/lib/agentf/agents/debugger.rb +75 -0
- data/lib/agentf/agents/designer.rb +69 -0
- data/lib/agentf/agents/documenter.rb +58 -0
- data/lib/agentf/agents/explorer.rb +65 -0
- data/lib/agentf/agents/reviewer.rb +64 -0
- data/lib/agentf/agents/security.rb +84 -0
- data/lib/agentf/agents/specialist.rb +68 -0
- data/lib/agentf/agents/tester.rb +79 -0
- data/lib/agentf/agents.rb +19 -0
- data/lib/agentf/cli/architecture.rb +83 -0
- data/lib/agentf/cli/arg_parser.rb +50 -0
- data/lib/agentf/cli/code.rb +165 -0
- data/lib/agentf/cli/install.rb +112 -0
- data/lib/agentf/cli/memory.rb +393 -0
- data/lib/agentf/cli/metrics.rb +103 -0
- data/lib/agentf/cli/router.rb +111 -0
- data/lib/agentf/cli/update.rb +204 -0
- data/lib/agentf/commands/architecture.rb +183 -0
- data/lib/agentf/commands/debugger.rb +238 -0
- data/lib/agentf/commands/designer.rb +179 -0
- data/lib/agentf/commands/explorer.rb +208 -0
- data/lib/agentf/commands/memory_reviewer.rb +186 -0
- data/lib/agentf/commands/metrics.rb +272 -0
- data/lib/agentf/commands/security_scanner.rb +98 -0
- data/lib/agentf/commands/tester.rb +232 -0
- data/lib/agentf/commands.rb +17 -0
- data/lib/agentf/context_builder.rb +35 -0
- data/lib/agentf/installer.rb +580 -0
- data/lib/agentf/mcp/server.rb +310 -0
- data/lib/agentf/memory.rb +530 -0
- data/lib/agentf/packs.rb +74 -0
- data/lib/agentf/service/providers.rb +158 -0
- data/lib/agentf/tools/component_spec.rb +28 -0
- data/lib/agentf/tools/error_analysis.rb +19 -0
- data/lib/agentf/tools/file_match.rb +21 -0
- data/lib/agentf/tools/test_template.rb +17 -0
- data/lib/agentf/tools.rb +12 -0
- data/lib/agentf/version.rb +5 -0
- data/lib/agentf/workflow_contract.rb +158 -0
- data/lib/agentf/workflow_engine.rb +424 -0
- data/lib/agentf.rb +87 -0
- metadata +164 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mcp"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Agentf
|
|
7
|
+
module MCP
|
|
8
|
+
# Pure-Ruby MCP server for Agentf.
|
|
9
|
+
#
|
|
10
|
+
# Replaces the Node.js mcp-server/ sidecar. Runs over stdio and exposes
|
|
11
|
+
# the same 9 tools the Node.js version did, but calls Ruby classes
|
|
12
|
+
# directly instead of shelling out to CLI binaries.
|
|
13
|
+
#
|
|
14
|
+
# Guardrails (env vars):
|
|
15
|
+
# AGENTF_MCP_ALLOWED_TOOLS - comma-separated allowlist, or * for all
|
|
16
|
+
# AGENTF_MCP_ALLOW_WRITES - true/false, controls memory write tools
|
|
17
|
+
# AGENTF_MCP_MAX_ARG_LENGTH - max length per string argument
|
|
18
|
+
class Server
|
|
19
|
+
KNOWN_TOOLS = %w[
|
|
20
|
+
code_glob
|
|
21
|
+
code_grep
|
|
22
|
+
code_tree
|
|
23
|
+
code_related_files
|
|
24
|
+
architecture_analyze_layers
|
|
25
|
+
memory_recent
|
|
26
|
+
memory_search
|
|
27
|
+
memory_add_lesson
|
|
28
|
+
memory_add_success
|
|
29
|
+
memory_add_pitfall
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
WRITE_TOOLS = Set.new(%w[
|
|
33
|
+
memory_add_lesson
|
|
34
|
+
memory_add_success
|
|
35
|
+
memory_add_pitfall
|
|
36
|
+
]).freeze
|
|
37
|
+
|
|
38
|
+
attr_reader :server, :guardrails
|
|
39
|
+
|
|
40
|
+
def initialize(explorer: nil, reviewer: nil, memory: nil, env: ENV)
|
|
41
|
+
@explorer = explorer || Agentf::Commands::Explorer.new
|
|
42
|
+
@architecture = Agentf::Commands::Architecture.new
|
|
43
|
+
@reviewer = reviewer || Agentf::Commands::MemoryReviewer.new
|
|
44
|
+
@memory = memory || Agentf::Memory::RedisMemory.new
|
|
45
|
+
@guardrails = build_guardrails(env)
|
|
46
|
+
@server = build_server
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Start the stdio read loop (blocks until stdin closes).
|
|
50
|
+
def run
|
|
51
|
+
@server.run
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# ── Guardrails ──────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
def build_guardrails(env)
|
|
59
|
+
{
|
|
60
|
+
allowed_tools: parse_allowed_tools(env.fetch("AGENTF_MCP_ALLOWED_TOOLS", nil)),
|
|
61
|
+
allow_writes: parse_boolean_env(env.fetch("AGENTF_MCP_ALLOW_WRITES", nil), true),
|
|
62
|
+
max_arg_length: parse_integer_env(env.fetch("AGENTF_MCP_MAX_ARG_LENGTH", nil), 4096)
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def parse_allowed_tools(value)
|
|
67
|
+
return Set.new(KNOWN_TOOLS) if value.nil? || value.strip.empty?
|
|
68
|
+
|
|
69
|
+
requested = value.split(",").map(&:strip).reject(&:empty?)
|
|
70
|
+
return Set.new(KNOWN_TOOLS) if requested.include?("*")
|
|
71
|
+
|
|
72
|
+
unknown = requested - KNOWN_TOOLS
|
|
73
|
+
raise ArgumentError, "Unknown tool(s) in AGENTF_MCP_ALLOWED_TOOLS: #{unknown.join(", ")}" unless unknown.empty?
|
|
74
|
+
|
|
75
|
+
Set.new(requested)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def parse_boolean_env(value, default)
|
|
79
|
+
return default if value.nil? || value.strip.empty?
|
|
80
|
+
|
|
81
|
+
case value.strip.downcase
|
|
82
|
+
when "1", "true", "yes", "on" then true
|
|
83
|
+
when "0", "false", "no", "off" then false
|
|
84
|
+
else default
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def parse_integer_env(value, default, min: 1)
|
|
89
|
+
return default if value.nil? || value.to_s.strip.empty?
|
|
90
|
+
|
|
91
|
+
parsed = Integer(value.to_s.strip, 10)
|
|
92
|
+
parsed >= min ? parsed : default
|
|
93
|
+
rescue ArgumentError
|
|
94
|
+
default
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def assert_tool_allowed!(tool_name)
|
|
98
|
+
return if @guardrails[:allowed_tools].include?(tool_name)
|
|
99
|
+
|
|
100
|
+
raise "Tool not allowed: #{tool_name}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def assert_write_allowed!(tool_name)
|
|
104
|
+
return unless WRITE_TOOLS.include?(tool_name)
|
|
105
|
+
return if @guardrails[:allow_writes]
|
|
106
|
+
|
|
107
|
+
raise "Write tools are disabled: #{tool_name}"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def assert_valid_string_args!(**args)
|
|
111
|
+
max = @guardrails[:max_arg_length]
|
|
112
|
+
args.each do |key, value|
|
|
113
|
+
next unless value.is_a?(String)
|
|
114
|
+
|
|
115
|
+
raise "Argument '#{key}' exceeds max length of #{max}" if value.length > max
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# ── Server construction ─────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
def build_server
|
|
122
|
+
s = ::MCP::Server.new(name: "agentf", version: Agentf::VERSION)
|
|
123
|
+
register_code_tools(s)
|
|
124
|
+
register_memory_tools(s)
|
|
125
|
+
register_architecture_tools(s)
|
|
126
|
+
s
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# ── Code tools ─────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
def register_code_tools(s)
|
|
132
|
+
explorer = @explorer
|
|
133
|
+
mcp_server = self
|
|
134
|
+
|
|
135
|
+
s.tool("code_glob") do
|
|
136
|
+
description "Find files using project glob patterns."
|
|
137
|
+
argument :pattern, String, required: true, description: "Glob pattern, e.g. lib/**/*.rb"
|
|
138
|
+
argument :types, Array, required: false, items: String, description: "File extensions to filter, e.g. [\"rb\",\"py\"]"
|
|
139
|
+
call do |args|
|
|
140
|
+
mcp_server.send(:guard!, "code_glob", **args)
|
|
141
|
+
file_types = args[:types]&.empty? ? nil : args[:types]
|
|
142
|
+
results = explorer.glob(args[:pattern], file_types: file_types)
|
|
143
|
+
JSON.generate(pattern: args[:pattern], matches: results, count: results.length)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
s.tool("code_grep") do
|
|
148
|
+
description "Search file contents with regex."
|
|
149
|
+
argument :pattern, String, required: true, description: "Regex or text to search"
|
|
150
|
+
argument :file_pattern, String, required: false, description: "Include pattern, e.g. *.rb"
|
|
151
|
+
argument :context_lines, Integer, required: false, description: "Context lines (0-20)"
|
|
152
|
+
call do |args|
|
|
153
|
+
mcp_server.send(:guard!, "code_grep", **args)
|
|
154
|
+
ctx = args[:context_lines] || 2
|
|
155
|
+
matches = explorer.grep(args[:pattern], file_pattern: args[:file_pattern], context_lines: ctx)
|
|
156
|
+
serialized = matches.map { |m| m.respond_to?(:to_h) ? m.to_h : m }
|
|
157
|
+
JSON.generate(pattern: args[:pattern], matches: serialized, count: serialized.length)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
s.tool("code_tree") do
|
|
162
|
+
description "Get directory tree structure."
|
|
163
|
+
argument :depth, Integer, required: false, description: "Max traversal depth (1-10)"
|
|
164
|
+
call do |args|
|
|
165
|
+
mcp_server.send(:guard!, "code_tree", **args)
|
|
166
|
+
max_depth = args[:depth] || 3
|
|
167
|
+
tree = explorer.get_file_tree(max_depth: max_depth)
|
|
168
|
+
JSON.generate(max_depth: max_depth, tree: tree)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
s.tool("code_related_files") do
|
|
173
|
+
description "Find imports and related files for a target file."
|
|
174
|
+
argument :target_file, String, required: true, description: "Workspace-relative file path"
|
|
175
|
+
call do |args|
|
|
176
|
+
mcp_server.send(:guard!, "code_related_files", **args)
|
|
177
|
+
related = explorer.find_related_files(args[:target_file])
|
|
178
|
+
JSON.generate(target_file: args[:target_file], related: related)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# ── Memory tools ───────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
def register_memory_tools(s)
|
|
186
|
+
reviewer = @reviewer
|
|
187
|
+
memory = @memory
|
|
188
|
+
mcp_server = self
|
|
189
|
+
|
|
190
|
+
s.tool("memory_recent") do
|
|
191
|
+
description "Get recent memories from Redis."
|
|
192
|
+
argument :limit, Integer, required: false, description: "How many memories to return (1-100)"
|
|
193
|
+
call do |args|
|
|
194
|
+
mcp_server.send(:guard!, "memory_recent", **args)
|
|
195
|
+
result = reviewer.get_recent_memories(limit: args[:limit] || 10)
|
|
196
|
+
JSON.generate(result)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
s.tool("memory_search") do
|
|
201
|
+
description "Search memories by keyword."
|
|
202
|
+
argument :query, String, required: true, description: "Search query"
|
|
203
|
+
argument :limit, Integer, required: false, description: "How many results to return (1-100)"
|
|
204
|
+
call do |args|
|
|
205
|
+
mcp_server.send(:guard!, "memory_search", **args)
|
|
206
|
+
result = reviewer.search(args[:query], limit: args[:limit] || 10)
|
|
207
|
+
JSON.generate(result)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
s.tool("memory_add_lesson") do
|
|
212
|
+
description "Store a lesson memory in Redis."
|
|
213
|
+
argument :title, String, required: true, description: "Lesson title"
|
|
214
|
+
argument :description, String, required: true, description: "Lesson description"
|
|
215
|
+
argument :agent, String, required: false, description: "Agent name"
|
|
216
|
+
argument :tags, Array, required: false, items: String, description: "Tags"
|
|
217
|
+
argument :context, String, required: false, description: "Context"
|
|
218
|
+
call do |args|
|
|
219
|
+
mcp_server.send(:guard!, "memory_add_lesson", **args)
|
|
220
|
+
id = memory.store_episode(
|
|
221
|
+
type: "lesson",
|
|
222
|
+
title: args[:title],
|
|
223
|
+
description: args[:description],
|
|
224
|
+
agent: args[:agent] || "SPECIALIST",
|
|
225
|
+
tags: args[:tags] || [],
|
|
226
|
+
context: args[:context].to_s,
|
|
227
|
+
code_snippet: ""
|
|
228
|
+
)
|
|
229
|
+
JSON.generate(id: id, type: "lesson", status: "stored")
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
s.tool("memory_add_success") do
|
|
234
|
+
description "Store a success memory in Redis."
|
|
235
|
+
argument :title, String, required: true, description: "Success title"
|
|
236
|
+
argument :description, String, required: true, description: "Success description"
|
|
237
|
+
argument :agent, String, required: false, description: "Agent name"
|
|
238
|
+
argument :tags, Array, required: false, items: String, description: "Tags"
|
|
239
|
+
argument :context, String, required: false, description: "Context"
|
|
240
|
+
call do |args|
|
|
241
|
+
mcp_server.send(:guard!, "memory_add_success", **args)
|
|
242
|
+
id = memory.store_episode(
|
|
243
|
+
type: "success",
|
|
244
|
+
title: args[:title],
|
|
245
|
+
description: args[:description],
|
|
246
|
+
agent: args[:agent] || "SPECIALIST",
|
|
247
|
+
tags: args[:tags] || [],
|
|
248
|
+
context: args[:context].to_s,
|
|
249
|
+
code_snippet: ""
|
|
250
|
+
)
|
|
251
|
+
JSON.generate(id: id, type: "success", status: "stored")
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
s.tool("memory_add_pitfall") do
|
|
256
|
+
description "Store a pitfall memory in Redis."
|
|
257
|
+
argument :title, String, required: true, description: "Pitfall title"
|
|
258
|
+
argument :description, String, required: true, description: "Pitfall description"
|
|
259
|
+
argument :agent, String, required: false, description: "Agent name"
|
|
260
|
+
argument :tags, Array, required: false, items: String, description: "Tags"
|
|
261
|
+
argument :context, String, required: false, description: "Context"
|
|
262
|
+
call do |args|
|
|
263
|
+
mcp_server.send(:guard!, "memory_add_pitfall", **args)
|
|
264
|
+
id = memory.store_episode(
|
|
265
|
+
type: "pitfall",
|
|
266
|
+
title: args[:title],
|
|
267
|
+
description: args[:description],
|
|
268
|
+
agent: args[:agent] || "SPECIALIST",
|
|
269
|
+
tags: args[:tags] || [],
|
|
270
|
+
context: args[:context].to_s,
|
|
271
|
+
code_snippet: ""
|
|
272
|
+
)
|
|
273
|
+
JSON.generate(id: id, type: "pitfall", status: "stored")
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def register_architecture_tools(s)
|
|
279
|
+
architecture = @architecture
|
|
280
|
+
mcp_server = self
|
|
281
|
+
|
|
282
|
+
s.tool("architecture_analyze_layers") do
|
|
283
|
+
description "Analyze architecture layers, review violations, or create gradual adoption plans."
|
|
284
|
+
argument :mode, String, required: false, description: "analyze|review|gradual"
|
|
285
|
+
argument :limit, Integer, required: false, description: "Maximum violations to return for review mode"
|
|
286
|
+
argument :goal, String, required: false, description: "Adoption goal for gradual mode"
|
|
287
|
+
call do |args|
|
|
288
|
+
mcp_server.send(:guard!, "architecture_analyze_layers", **args)
|
|
289
|
+
case (args[:mode] || "analyze").to_s
|
|
290
|
+
when "review"
|
|
291
|
+
JSON.generate(architecture.review_layer_violations(limit: args[:limit] || 20))
|
|
292
|
+
when "gradual"
|
|
293
|
+
JSON.generate(architecture.plan_gradual_adoption(goal: args[:goal] || "improve architecture boundaries"))
|
|
294
|
+
else
|
|
295
|
+
JSON.generate(architecture.analyze_layers)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# ── Shared guard helper ────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
def guard!(tool_name, **args)
|
|
304
|
+
assert_tool_allowed!(tool_name)
|
|
305
|
+
assert_write_allowed!(tool_name)
|
|
306
|
+
assert_valid_string_args!(**args)
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|