agent-harness 0.11.1 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +7 -0
- data/lib/agent_harness/configuration.rb +16 -0
- data/lib/agent_harness/mcp_config_loader.rb +62 -0
- data/lib/agent_harness/mcp_config_translator.rb +85 -0
- data/lib/agent_harness/mcp_server.rb +40 -14
- data/lib/agent_harness/providers/anthropic.rb +3 -79
- data/lib/agent_harness/providers/base.rb +7 -1
- data/lib/agent_harness/providers/codex.rb +32 -1
- data/lib/agent_harness/providers/mcp_config_file_support.rb +76 -0
- data/lib/agent_harness/version.rb +1 -1
- data/lib/agent_harness.rb +3 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 50b5bc213cf4a1b6f9441a06a82c38210c7e6907eb085e6cadf71e305e4b7897
|
|
4
|
+
data.tar.gz: 9785ea0b1b35f5aa52528741ce070287f2d433235ffeaf3d6df0ab7337f486b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2343d812d85375faad3a55e0462eb228003ff5eddc1920fd1edb75aedf381e5b86e3062c191ee64d24c39f245c1f27ded7d0e74247526727901b6b5391c2518a
|
|
7
|
+
data.tar.gz: cfe73ff0ffce00f5e0be1726b3dc28d0cdb73aab6546988c19b79846c1d16014476302f2d258d13bb703124edbf86066a96cd43044047ebacc81ad0cf5c853e5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.11.2](https://github.com/viamin/agent-harness/compare/agent-harness/v0.11.1...agent-harness/v0.11.2) (2026-04-27)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* 162: Support provider-agnostic MCP configuration ([#165](https://github.com/viamin/agent-harness/issues/165)) ([27f4814](https://github.com/viamin/agent-harness/commit/27f48146e99d8fdba0346235a3e5f19138266652))
|
|
9
|
+
|
|
3
10
|
## [0.11.1](https://github.com/viamin/agent-harness/compare/agent-harness/v0.11.0...agent-harness/v0.11.1) (2026-04-26)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -78,6 +78,22 @@ module AgentHarness
|
|
|
78
78
|
@custom_provider_classes[name.to_sym] = klass
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
+
# Set MCP servers from an array of server definitions.
|
|
82
|
+
#
|
|
83
|
+
# @param servers [Array<Hash, McpServer>] server definitions
|
|
84
|
+
def mcp_servers=(servers)
|
|
85
|
+
normalized = McpConfigTranslator.normalize_servers(servers)
|
|
86
|
+
@mcp_servers = normalized.each_with_object({}) { |s, h| h[s.name.to_sym] = s }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Load MCP server definitions from a configuration file.
|
|
90
|
+
#
|
|
91
|
+
# @param path [String] file path
|
|
92
|
+
def load_mcp_servers_file(path)
|
|
93
|
+
loaded = McpConfigLoader.load_file(path)
|
|
94
|
+
@mcp_servers = loaded.each_with_object({}) { |s, h| h[s.name.to_sym] = s }
|
|
95
|
+
end
|
|
96
|
+
|
|
81
97
|
# Configure a provider-agnostic sub-agent definition.
|
|
82
98
|
#
|
|
83
99
|
# @param name [Symbol, String] sub-agent name
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module AgentHarness
|
|
7
|
+
class McpConfigLoader
|
|
8
|
+
ENV_VAR_PATTERN = /\$\{([A-Z0-9_]+)\}/
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def load_file(path)
|
|
12
|
+
parsed = parse_file(path)
|
|
13
|
+
servers = parsed.is_a?(Hash) ? (parsed["servers"] || parsed[:servers] || parsed) : parsed
|
|
14
|
+
|
|
15
|
+
unless servers.is_a?(Array)
|
|
16
|
+
raise McpConfigurationError,
|
|
17
|
+
"MCP config file must contain a top-level servers array"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
servers.map do |server|
|
|
21
|
+
McpServer.from_hash(interpolate_env(server))
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def parse_file(path)
|
|
28
|
+
ext = File.extname(path).downcase
|
|
29
|
+
content = File.read(path)
|
|
30
|
+
|
|
31
|
+
case ext
|
|
32
|
+
when ".json"
|
|
33
|
+
JSON.parse(content)
|
|
34
|
+
when ".yml", ".yaml"
|
|
35
|
+
YAML.safe_load(content, aliases: false) || {}
|
|
36
|
+
else
|
|
37
|
+
raise McpConfigurationError,
|
|
38
|
+
"Unsupported MCP config file format '#{ext}'. Use .json, .yml, or .yaml"
|
|
39
|
+
end
|
|
40
|
+
rescue Errno::ENOENT => e
|
|
41
|
+
raise McpConfigurationError, "MCP config file not found: #{e.message}"
|
|
42
|
+
rescue JSON::ParserError, Psych::SyntaxError => e
|
|
43
|
+
raise McpConfigurationError, "Failed to parse MCP config file: #{e.message}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def interpolate_env(value)
|
|
47
|
+
case value
|
|
48
|
+
when Array
|
|
49
|
+
value.map { |entry| interpolate_env(entry) }
|
|
50
|
+
when Hash
|
|
51
|
+
value.each_with_object({}) do |(key, entry), memo|
|
|
52
|
+
memo[key] = interpolate_env(entry)
|
|
53
|
+
end
|
|
54
|
+
when String
|
|
55
|
+
value.gsub(ENV_VAR_PATTERN) { ENV.fetch(Regexp.last_match(1), "") }
|
|
56
|
+
else
|
|
57
|
+
value
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentHarness
|
|
4
|
+
module McpConfigTranslator
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def for_provider(provider, mcp_servers)
|
|
8
|
+
servers = normalize_servers(mcp_servers)
|
|
9
|
+
|
|
10
|
+
case provider.to_sym
|
|
11
|
+
when :anthropic, :claude, :claude_code
|
|
12
|
+
translate_for_claude(servers)
|
|
13
|
+
when :codex
|
|
14
|
+
translate_for_codex(servers)
|
|
15
|
+
when :openai
|
|
16
|
+
translate_for_openai(servers)
|
|
17
|
+
else
|
|
18
|
+
servers.map(&:to_h)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def normalize_servers(mcp_servers)
|
|
23
|
+
Array(mcp_servers).map do |server|
|
|
24
|
+
case server
|
|
25
|
+
when McpServer
|
|
26
|
+
server
|
|
27
|
+
when Hash
|
|
28
|
+
McpServer.from_hash(server)
|
|
29
|
+
else
|
|
30
|
+
raise McpConfigurationError, "MCP server must be a Hash or McpServer, got #{server.class}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def translate_for_claude(mcp_servers)
|
|
36
|
+
{
|
|
37
|
+
mcpServers: mcp_servers.each_with_object({}) do |server, memo|
|
|
38
|
+
entry = server.stdio? ? {command: server.command} : {url: server.url}
|
|
39
|
+
entry[:args] = server.args if server.stdio? && !server.args.empty?
|
|
40
|
+
entry[:env] = server.env unless server.env.empty?
|
|
41
|
+
entry[:headers] = server.headers if server.http? && !server.headers.empty?
|
|
42
|
+
memo[server.name] = entry
|
|
43
|
+
end
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def translate_for_codex(mcp_servers)
|
|
48
|
+
{
|
|
49
|
+
mcp_servers: mcp_servers.each_with_object({}) do |server, memo|
|
|
50
|
+
entry = server.stdio? ? {command: server.command, transport: server.transport} : {url: server.url, transport: server.transport}
|
|
51
|
+
entry[:args] = server.args if server.stdio? && !server.args.empty?
|
|
52
|
+
entry[:env] = server.env unless server.env.empty?
|
|
53
|
+
entry[:headers] = server.headers if server.http? && !server.headers.empty?
|
|
54
|
+
memo[server.name] = entry
|
|
55
|
+
end
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def translate_for_openai(mcp_servers)
|
|
60
|
+
mcp_servers.map do |server|
|
|
61
|
+
unless server.http?
|
|
62
|
+
raise McpTransportUnsupportedError.new(
|
|
63
|
+
"Provider 'openai' only supports remote MCP servers over HTTP/SSE (server: '#{server.name}')",
|
|
64
|
+
provider: :openai
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
unsupported_headers = server.headers.keys - ["Authorization"]
|
|
69
|
+
unless unsupported_headers.empty?
|
|
70
|
+
raise McpConfigurationError,
|
|
71
|
+
"OpenAI MCP translation only supports the Authorization header (server: '#{server.name}')"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
tool = {
|
|
75
|
+
type: "mcp",
|
|
76
|
+
server_label: server.name,
|
|
77
|
+
server_url: server.url,
|
|
78
|
+
require_approval: "never"
|
|
79
|
+
}
|
|
80
|
+
tool[:authorization] = server.headers["Authorization"] if server.headers.key?("Authorization")
|
|
81
|
+
tool
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -10,7 +10,8 @@ module AgentHarness
|
|
|
10
10
|
# McpServer.new(
|
|
11
11
|
# name: "filesystem",
|
|
12
12
|
# transport: "stdio",
|
|
13
|
-
# command:
|
|
13
|
+
# command: "npx",
|
|
14
|
+
# args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
|
|
14
15
|
# env: { "DEBUG" => "0" }
|
|
15
16
|
# )
|
|
16
17
|
#
|
|
@@ -23,21 +24,24 @@ module AgentHarness
|
|
|
23
24
|
class McpServer
|
|
24
25
|
VALID_TRANSPORTS = %w[stdio http sse].freeze
|
|
25
26
|
|
|
26
|
-
attr_reader :name, :transport, :command, :args, :env, :url
|
|
27
|
+
attr_reader :name, :transport, :command, :args, :env, :url, :headers
|
|
27
28
|
|
|
28
29
|
# @param name [String] unique name for this MCP server
|
|
29
30
|
# @param transport [String] one of "stdio", "http", "sse"
|
|
30
|
-
# @param command [Array<String>, nil]
|
|
31
|
+
# @param command [String, Array<String>, nil] executable to launch (stdio only).
|
|
32
|
+
# Arrays are accepted for backwards compatibility and normalized into
|
|
33
|
+
# +command+ plus +args+.
|
|
31
34
|
# @param args [Array<String>, nil] additional args for the command
|
|
32
35
|
# @param env [Hash<String,String>, nil] environment variables for the server process
|
|
33
36
|
# @param url [String, nil] URL for HTTP/SSE transport
|
|
34
|
-
|
|
37
|
+
# @param headers [Hash<String,String>, nil] HTTP headers for remote MCP servers
|
|
38
|
+
def initialize(name:, transport:, command: nil, args: nil, env: nil, url: nil, headers: nil)
|
|
35
39
|
@name = name
|
|
36
40
|
@transport = transport.to_s
|
|
37
|
-
@command = command
|
|
38
|
-
@args = args || []
|
|
41
|
+
@command, @args = normalize_command_and_args(command, args)
|
|
39
42
|
@env = env || {}
|
|
40
43
|
@url = url
|
|
44
|
+
@headers = headers || {}
|
|
41
45
|
|
|
42
46
|
validate!
|
|
43
47
|
end
|
|
@@ -63,7 +67,8 @@ module AgentHarness
|
|
|
63
67
|
command: hash[:command],
|
|
64
68
|
args: hash[:args],
|
|
65
69
|
env: hash[:env],
|
|
66
|
-
url: hash[:url]
|
|
70
|
+
url: hash[:url],
|
|
71
|
+
headers: hash[:headers]
|
|
67
72
|
)
|
|
68
73
|
end
|
|
69
74
|
|
|
@@ -75,6 +80,12 @@ module AgentHarness
|
|
|
75
80
|
%w[http sse].include?(@transport)
|
|
76
81
|
end
|
|
77
82
|
|
|
83
|
+
def command_argv
|
|
84
|
+
return [] unless stdio?
|
|
85
|
+
|
|
86
|
+
[@command, *@args]
|
|
87
|
+
end
|
|
88
|
+
|
|
78
89
|
# Check if the MCP server is reachable based on its transport type.
|
|
79
90
|
#
|
|
80
91
|
# For stdio servers, checks that a command is present.
|
|
@@ -101,6 +112,7 @@ module AgentHarness
|
|
|
101
112
|
h[:args] = @args unless @args.empty?
|
|
102
113
|
else
|
|
103
114
|
h[:url] = @url
|
|
115
|
+
h[:headers] = @headers unless @headers.empty?
|
|
104
116
|
end
|
|
105
117
|
h[:env] = @env unless @env.empty?
|
|
106
118
|
h
|
|
@@ -118,6 +130,7 @@ module AgentHarness
|
|
|
118
130
|
|
|
119
131
|
validate_args!
|
|
120
132
|
validate_env!
|
|
133
|
+
validate_headers!
|
|
121
134
|
validate_stdio! if stdio?
|
|
122
135
|
validate_http! if http?
|
|
123
136
|
validate_no_stdio_only_fields_on_http! if http?
|
|
@@ -137,15 +150,17 @@ module AgentHarness
|
|
|
137
150
|
"MCP server '#{@name}' env must be a Hash with String keys and values"
|
|
138
151
|
end
|
|
139
152
|
|
|
140
|
-
def
|
|
141
|
-
if @
|
|
142
|
-
raise McpConfigurationError,
|
|
143
|
-
"MCP server '#{@name}' with stdio transport requires a non-empty command array"
|
|
144
|
-
end
|
|
153
|
+
def validate_headers!
|
|
154
|
+
return if @headers.is_a?(Hash) && @headers.keys.all? { |k| k.is_a?(String) } && @headers.values.all? { |v| v.is_a?(String) }
|
|
145
155
|
|
|
146
|
-
|
|
156
|
+
raise McpConfigurationError,
|
|
157
|
+
"MCP server '#{@name}' headers must be a Hash with String keys and values"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def validate_stdio!
|
|
161
|
+
if @command.nil? || !@command.is_a?(String) || @command.strip.empty?
|
|
147
162
|
raise McpConfigurationError,
|
|
148
|
-
"MCP server '#{@name}'
|
|
163
|
+
"MCP server '#{@name}' with stdio transport requires a non-empty command string"
|
|
149
164
|
end
|
|
150
165
|
|
|
151
166
|
return if @url.nil?
|
|
@@ -173,6 +188,17 @@ module AgentHarness
|
|
|
173
188
|
"MCP server '#{@name}' with #{@transport} transport should not have args (args are only valid for stdio)"
|
|
174
189
|
end
|
|
175
190
|
|
|
191
|
+
def normalize_command_and_args(command, args)
|
|
192
|
+
normalized_args = args || []
|
|
193
|
+
return [command, normalized_args] unless command.is_a?(Array)
|
|
194
|
+
|
|
195
|
+
unless normalized_args.is_a?(Array)
|
|
196
|
+
return [command.first, normalized_args]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
[command.first, command[1..] + normalized_args]
|
|
200
|
+
end
|
|
201
|
+
|
|
176
202
|
def http_ping_ok?(timeout: 5)
|
|
177
203
|
require "net/http"
|
|
178
204
|
uri = URI.parse(@url)
|
|
@@ -15,6 +15,7 @@ module AgentHarness
|
|
|
15
15
|
# response = provider.send_message(prompt: "Hello!")
|
|
16
16
|
class Anthropic < Base
|
|
17
17
|
include RateLimitResetParsing
|
|
18
|
+
include McpConfigFileSupport
|
|
18
19
|
|
|
19
20
|
# Model name pattern for Anthropic Claude models
|
|
20
21
|
MODEL_PATTERN = /^claude-[\d.-]+-(?:opus|sonnet|haiku)(?:-\d{8})?$/i
|
|
@@ -756,85 +757,8 @@ module AgentHarness
|
|
|
756
757
|
servers
|
|
757
758
|
end
|
|
758
759
|
|
|
759
|
-
def
|
|
760
|
-
|
|
761
|
-
require "tmpdir"
|
|
762
|
-
require "securerandom"
|
|
763
|
-
|
|
764
|
-
config = build_claude_mcp_config(mcp_servers)
|
|
765
|
-
config_json = JSON.generate(config)
|
|
766
|
-
|
|
767
|
-
if @executor.is_a?(DockerCommandExecutor)
|
|
768
|
-
# When running inside a Docker container, write the config file
|
|
769
|
-
# inside the container so the CLI process can read it.
|
|
770
|
-
# Track the path so cleanup_mcp_tempfiles! can remove it after execution.
|
|
771
|
-
container_path = "/tmp/agent_harness_mcp_#{SecureRandom.hex(8)}.json"
|
|
772
|
-
result = @executor.execute(
|
|
773
|
-
["sh", "-c", "cat > #{container_path}"],
|
|
774
|
-
stdin_data: config_json,
|
|
775
|
-
timeout: 5
|
|
776
|
-
)
|
|
777
|
-
unless result.success?
|
|
778
|
-
raise McpConfigurationError,
|
|
779
|
-
"Failed to write MCP config inside container: #{result.stderr}"
|
|
780
|
-
end
|
|
781
|
-
|
|
782
|
-
@mcp_docker_config_paths ||= []
|
|
783
|
-
@mcp_docker_config_paths << container_path
|
|
784
|
-
|
|
785
|
-
container_path
|
|
786
|
-
else
|
|
787
|
-
dir = working_dir || Dir.tmpdir
|
|
788
|
-
file = Tempfile.new(["agent_harness_mcp_", ".json"], dir)
|
|
789
|
-
file.write(config_json)
|
|
790
|
-
file.close
|
|
791
|
-
|
|
792
|
-
# Hold a reference so the Tempfile is not garbage-collected (and
|
|
793
|
-
# therefore deleted) before the CLI process reads it.
|
|
794
|
-
# Cleaned up by cleanup_mcp_tempfiles! after execution.
|
|
795
|
-
@mcp_config_tempfiles ||= []
|
|
796
|
-
@mcp_config_tempfiles << file
|
|
797
|
-
|
|
798
|
-
file.path
|
|
799
|
-
end
|
|
800
|
-
end
|
|
801
|
-
|
|
802
|
-
def build_claude_mcp_config(mcp_servers)
|
|
803
|
-
servers = {}
|
|
804
|
-
mcp_servers.each do |server|
|
|
805
|
-
h = if server.stdio?
|
|
806
|
-
entry = {command: server.command.first}
|
|
807
|
-
remaining_args = server.command[1..] + server.args
|
|
808
|
-
entry[:args] = remaining_args unless remaining_args.empty?
|
|
809
|
-
entry
|
|
810
|
-
else
|
|
811
|
-
{url: server.url}
|
|
812
|
-
end
|
|
813
|
-
h[:env] = server.env unless server.env.empty?
|
|
814
|
-
servers[server.name] = h
|
|
815
|
-
end
|
|
816
|
-
{mcpServers: servers}
|
|
817
|
-
end
|
|
818
|
-
|
|
819
|
-
def cleanup_mcp_tempfiles!
|
|
820
|
-
if @mcp_config_tempfiles
|
|
821
|
-
@mcp_config_tempfiles.each do |file|
|
|
822
|
-
file.close unless file.closed?
|
|
823
|
-
file.unlink
|
|
824
|
-
rescue
|
|
825
|
-
nil
|
|
826
|
-
end
|
|
827
|
-
@mcp_config_tempfiles = nil
|
|
828
|
-
end
|
|
829
|
-
|
|
830
|
-
if @mcp_docker_config_paths
|
|
831
|
-
@mcp_docker_config_paths.each do |path|
|
|
832
|
-
@executor.execute(["rm", "-f", path], timeout: 5)
|
|
833
|
-
rescue
|
|
834
|
-
nil
|
|
835
|
-
end
|
|
836
|
-
@mcp_docker_config_paths = nil
|
|
837
|
-
end
|
|
760
|
+
def mcp_provider_key
|
|
761
|
+
:anthropic
|
|
838
762
|
end
|
|
839
763
|
|
|
840
764
|
def build_tool_control_flags(tools_option, skip_permission_mode: false)
|
|
@@ -419,7 +419,13 @@ module AgentHarness
|
|
|
419
419
|
end
|
|
420
420
|
|
|
421
421
|
def normalize_mcp_servers(options)
|
|
422
|
-
|
|
422
|
+
if options.key?(:mcp_servers)
|
|
423
|
+
servers = options[:mcp_servers]
|
|
424
|
+
else
|
|
425
|
+
# Configuration stores mcp_servers as a Hash keyed by name; extract values.
|
|
426
|
+
config_servers = AgentHarness.configuration.mcp_servers
|
|
427
|
+
servers = config_servers.is_a?(Hash) ? config_servers.values : config_servers
|
|
428
|
+
end
|
|
423
429
|
return options if servers.nil?
|
|
424
430
|
|
|
425
431
|
unless servers.is_a?(Array)
|
|
@@ -9,6 +9,7 @@ module AgentHarness
|
|
|
9
9
|
# Provides integration with the OpenAI Codex CLI tool.
|
|
10
10
|
class Codex < Base
|
|
11
11
|
include RateLimitResetParsing
|
|
12
|
+
include McpConfigFileSupport
|
|
12
13
|
|
|
13
14
|
SUPPORTED_CLI_VERSION = "0.116.0"
|
|
14
15
|
SUPPORTED_CLI_REQUIREMENT = Gem::Requirement.new(">= #{SUPPORTED_CLI_VERSION}", "< 0.117.0").freeze
|
|
@@ -186,7 +187,7 @@ module AgentHarness
|
|
|
186
187
|
vision: false,
|
|
187
188
|
tool_use: true,
|
|
188
189
|
json_mode: false,
|
|
189
|
-
mcp:
|
|
190
|
+
mcp: true,
|
|
190
191
|
dangerous_mode: true
|
|
191
192
|
}
|
|
192
193
|
end
|
|
@@ -199,6 +200,27 @@ module AgentHarness
|
|
|
199
200
|
|
|
200
201
|
def cli_env_overrides = {"PAID_CODEX_SUBSCRIPTION_AUTH" => "1"}
|
|
201
202
|
|
|
203
|
+
def send_message(prompt:, **options)
|
|
204
|
+
super
|
|
205
|
+
ensure
|
|
206
|
+
cleanup_mcp_tempfiles!
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def supports_mcp?
|
|
210
|
+
true
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def supported_mcp_transports
|
|
214
|
+
%w[stdio http sse]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def build_mcp_flags(mcp_servers, working_dir: nil)
|
|
218
|
+
return [] if mcp_servers.empty?
|
|
219
|
+
|
|
220
|
+
config_path = write_mcp_config_file(mcp_servers, working_dir: working_dir)
|
|
221
|
+
["--mcp-config", config_path]
|
|
222
|
+
end
|
|
223
|
+
|
|
202
224
|
def test_command_overrides
|
|
203
225
|
["--skip-git-repo-check", "--output-last-message"]
|
|
204
226
|
end
|
|
@@ -449,6 +471,11 @@ module AgentHarness
|
|
|
449
471
|
cmd += sandbox_bypass_flags
|
|
450
472
|
end
|
|
451
473
|
|
|
474
|
+
# Add MCP server flags (validated/normalized by Base#send_message)
|
|
475
|
+
if options[:mcp_servers]&.any?
|
|
476
|
+
cmd += build_mcp_flags(options[:mcp_servers])
|
|
477
|
+
end
|
|
478
|
+
|
|
452
479
|
if options[:session]
|
|
453
480
|
cmd += session_flags(options[:session])
|
|
454
481
|
end
|
|
@@ -1259,6 +1286,10 @@ module AgentHarness
|
|
|
1259
1286
|
config_dir = ENV["CODEX_CONFIG_DIR"] || File.expand_path("~/.codex")
|
|
1260
1287
|
File.join(config_dir, "config.json")
|
|
1261
1288
|
end
|
|
1289
|
+
|
|
1290
|
+
def mcp_provider_key
|
|
1291
|
+
:codex
|
|
1292
|
+
end
|
|
1262
1293
|
end
|
|
1263
1294
|
end
|
|
1264
1295
|
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentHarness
|
|
4
|
+
module Providers
|
|
5
|
+
# Shared concern for writing and cleaning up MCP configuration tempfiles.
|
|
6
|
+
#
|
|
7
|
+
# Include this module in any provider that needs to write MCP config files
|
|
8
|
+
# for CLI processes. The including class must define an +mcp_provider_key+
|
|
9
|
+
# method that returns the symbol passed to +McpConfigTranslator.for_provider+.
|
|
10
|
+
module McpConfigFileSupport
|
|
11
|
+
def write_mcp_config_file(mcp_servers, working_dir: nil)
|
|
12
|
+
require "tempfile"
|
|
13
|
+
require "tmpdir"
|
|
14
|
+
require "securerandom"
|
|
15
|
+
|
|
16
|
+
config = McpConfigTranslator.for_provider(mcp_provider_key, mcp_servers)
|
|
17
|
+
config_json = JSON.generate(config)
|
|
18
|
+
|
|
19
|
+
if @executor.is_a?(DockerCommandExecutor)
|
|
20
|
+
# When running inside a Docker container, write the config file
|
|
21
|
+
# inside the container so the CLI process can read it.
|
|
22
|
+
# Track the path so cleanup_mcp_tempfiles! can remove it after execution.
|
|
23
|
+
container_path = "/tmp/agent_harness_mcp_#{SecureRandom.hex(8)}.json"
|
|
24
|
+
result = @executor.execute(
|
|
25
|
+
["sh", "-c", "cat > #{container_path}"],
|
|
26
|
+
stdin_data: config_json,
|
|
27
|
+
timeout: 5
|
|
28
|
+
)
|
|
29
|
+
unless result.success?
|
|
30
|
+
raise McpConfigurationError,
|
|
31
|
+
"Failed to write MCP config inside container: #{result.stderr}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@mcp_docker_config_paths ||= []
|
|
35
|
+
@mcp_docker_config_paths << container_path
|
|
36
|
+
|
|
37
|
+
container_path
|
|
38
|
+
else
|
|
39
|
+
dir = working_dir || Dir.tmpdir
|
|
40
|
+
file = Tempfile.new(["agent_harness_mcp_", ".json"], dir)
|
|
41
|
+
file.write(config_json)
|
|
42
|
+
file.close
|
|
43
|
+
|
|
44
|
+
# Hold a reference so the Tempfile is not garbage-collected (and
|
|
45
|
+
# therefore deleted) before the CLI process reads it.
|
|
46
|
+
# Cleaned up by cleanup_mcp_tempfiles! after execution.
|
|
47
|
+
@mcp_config_tempfiles ||= []
|
|
48
|
+
@mcp_config_tempfiles << file
|
|
49
|
+
|
|
50
|
+
file.path
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def cleanup_mcp_tempfiles!
|
|
55
|
+
if @mcp_config_tempfiles
|
|
56
|
+
@mcp_config_tempfiles.each do |file|
|
|
57
|
+
file.close unless file.closed?
|
|
58
|
+
file.unlink
|
|
59
|
+
rescue
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
@mcp_config_tempfiles = nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if @mcp_docker_config_paths
|
|
66
|
+
@mcp_docker_config_paths.each do |path|
|
|
67
|
+
@executor.execute(["rm", "-f", path], timeout: 5)
|
|
68
|
+
rescue
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
@mcp_docker_config_paths = nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/agent_harness.rb
CHANGED
|
@@ -278,6 +278,8 @@ end
|
|
|
278
278
|
# Core components
|
|
279
279
|
require_relative "agent_harness/errors"
|
|
280
280
|
require_relative "agent_harness/mcp_server"
|
|
281
|
+
require_relative "agent_harness/mcp_config_loader"
|
|
282
|
+
require_relative "agent_harness/mcp_config_translator"
|
|
281
283
|
require_relative "agent_harness/sub_agent_config"
|
|
282
284
|
require_relative "agent_harness/sub_agent_file_loader"
|
|
283
285
|
require_relative "agent_harness/sub_agent_translator"
|
|
@@ -301,6 +303,7 @@ require_relative "agent_harness/providers/adapter"
|
|
|
301
303
|
require_relative "agent_harness/providers/base"
|
|
302
304
|
require_relative "agent_harness/providers/token_usage_parsing"
|
|
303
305
|
require_relative "agent_harness/providers/rate_limit_reset_parsing"
|
|
306
|
+
require_relative "agent_harness/providers/mcp_config_file_support"
|
|
304
307
|
require_relative "agent_harness/providers/anthropic"
|
|
305
308
|
require_relative "agent_harness/providers/aider"
|
|
306
309
|
require_relative "agent_harness/providers/codex"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: agent-harness
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.11.
|
|
4
|
+
version: 0.11.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -107,6 +107,8 @@ files:
|
|
|
107
107
|
- lib/agent_harness/error_taxonomy.rb
|
|
108
108
|
- lib/agent_harness/errors.rb
|
|
109
109
|
- lib/agent_harness/execution_preparation.rb
|
|
110
|
+
- lib/agent_harness/mcp_config_loader.rb
|
|
111
|
+
- lib/agent_harness/mcp_config_translator.rb
|
|
110
112
|
- lib/agent_harness/mcp_server.rb
|
|
111
113
|
- lib/agent_harness/openai_compatible_transport.rb
|
|
112
114
|
- lib/agent_harness/orchestration/circuit_breaker.rb
|
|
@@ -126,6 +128,7 @@ files:
|
|
|
126
128
|
- lib/agent_harness/providers/gemini.rb
|
|
127
129
|
- lib/agent_harness/providers/github_copilot.rb
|
|
128
130
|
- lib/agent_harness/providers/kilocode.rb
|
|
131
|
+
- lib/agent_harness/providers/mcp_config_file_support.rb
|
|
129
132
|
- lib/agent_harness/providers/mistral_vibe.rb
|
|
130
133
|
- lib/agent_harness/providers/opencode.rb
|
|
131
134
|
- lib/agent_harness/providers/rate_limit_reset_parsing.rb
|