agent-harness 0.11.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 635aae919a5bbbf99af4a24199b45507370c01ccbf637bfd8d9d0fa18bdb3c22
4
- data.tar.gz: 30548d834ae0195030e98565007ced6ebf140f12f9a489ae10a6d423c40e087f
3
+ metadata.gz: 50b5bc213cf4a1b6f9441a06a82c38210c7e6907eb085e6cadf71e305e4b7897
4
+ data.tar.gz: 9785ea0b1b35f5aa52528741ce070287f2d433235ffeaf3d6df0ab7337f486b0
5
5
  SHA512:
6
- metadata.gz: ac200425094b482ad90fd6492ba0bf4d612ed08560bacde517c988375d1d452b12aca7c34f8ed9519cf907daa87a07a96a4c044952126ce3cd9e37f2d6b9a788
7
- data.tar.gz: a871de9fcc11224506f4220025016b3b7201ef97bc1e1aca918562c1f983b9dd175a93dbd6315a71a3a270234d3b9cd019f7deaa82030b984e11434ca86328f8
6
+ metadata.gz: 2343d812d85375faad3a55e0462eb228003ff5eddc1920fd1edb75aedf381e5b86e3062c191ee64d24c39f245c1f27ded7d0e74247526727901b6b5391c2518a
7
+ data.tar.gz: cfe73ff0ffce00f5e0be1726b3dc28d0cdb73aab6546988c19b79846c1d16014476302f2d258d13bb703124edbf86066a96cd43044047ebacc81ad0cf5c853e5
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.11.0"
2
+ ".": "0.11.2"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
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
+
10
+ ## [0.11.1](https://github.com/viamin/agent-harness/compare/agent-harness/v0.11.0...agent-harness/v0.11.1) (2026-04-26)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * 163: Support provider-agnostic sub-agent definitions ([#166](https://github.com/viamin/agent-harness/issues/166)) ([1a00c35](https://github.com/viamin/agent-harness/commit/1a00c35f61c624b23f8d37b0d1b41e007d007ad3))
16
+
3
17
  ## [0.11.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.10.0...agent-harness/v0.11.0) (2026-04-25)
4
18
 
5
19
 
@@ -23,6 +23,7 @@ module AgentHarness
23
23
  attr_writer :command_executor
24
24
 
25
25
  attr_reader :providers, :orchestration_config, :callbacks, :custom_provider_classes
26
+ attr_reader :sub_agents, :tool_registry, :mcp_servers
26
27
 
27
28
  def initialize
28
29
  @logger = nil # Will use null logger if not set
@@ -36,6 +37,9 @@ module AgentHarness
36
37
  @orchestration_config = OrchestrationConfig.new
37
38
  @callbacks = CallbackRegistry.new
38
39
  @custom_provider_classes = {}
40
+ @sub_agents = {}
41
+ @tool_registry = ToolRegistry.new
42
+ @mcp_servers = {}
39
43
  end
40
44
 
41
45
  # Get or lazily initialize the command executor
@@ -74,6 +78,94 @@ module AgentHarness
74
78
  @custom_provider_classes[name.to_sym] = klass
75
79
  end
76
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
+
97
+ # Configure a provider-agnostic sub-agent definition.
98
+ #
99
+ # @param name [Symbol, String] sub-agent name
100
+ # @param attributes [Hash] sub-agent attributes
101
+ # @yield [Hash] mutable attributes hash before validation
102
+ # @return [SubAgentConfig] registered sub-agent config
103
+ def sub_agent(name, attributes = {})
104
+ attributes = attributes.dup
105
+ yield(attributes) if block_given?
106
+ attributes[:name] = name
107
+
108
+ config = SubAgentConfig.from_hash(attributes)
109
+ @sub_agents[config.name] = config
110
+ end
111
+
112
+ # Load sub-agent definitions from a YAML or Markdown file.
113
+ #
114
+ # @param path [String] file path
115
+ # @return [Array<SubAgentConfig>] loaded sub-agents
116
+ def load_sub_agents(path)
117
+ SubAgentFileLoader.load(path).each do |sub_agent|
118
+ @sub_agents[sub_agent.name] = sub_agent
119
+ end
120
+ end
121
+
122
+ # Register a generic tool definition that sub-agents can reference.
123
+ #
124
+ # @param name [Symbol, String] tool name
125
+ # @param description [String, nil] tool description
126
+ # @param provider_mappings [Hash] provider-specific mappings
127
+ # @return [ToolDefinition]
128
+ def register_tool(name, description: nil, **provider_mappings)
129
+ @tool_registry.register(name, description: description, **provider_mappings)
130
+ end
131
+
132
+ # Register a named MCP server for later reference by sub-agents.
133
+ #
134
+ # @param name [Symbol, String] server name
135
+ # @param definition [Hash, McpServer, nil] server definition
136
+ # @param attributes [Hash] server attributes
137
+ # @return [McpServer]
138
+ def register_mcp_server(name, definition = nil, **attributes)
139
+ server = if definition.is_a?(McpServer)
140
+ definition
141
+ else
142
+ payload = (definition || attributes).dup
143
+ payload[:name] ||= name.to_s
144
+ McpServer.from_hash(payload)
145
+ end
146
+
147
+ @mcp_servers[name.to_sym] = server
148
+ end
149
+
150
+ # Resolve a named or inline sub-agent definition.
151
+ #
152
+ # @param reference [Symbol, String, Hash, SubAgentConfig, nil] sub-agent reference
153
+ # @return [SubAgentConfig, nil] resolved sub-agent config
154
+ def resolve_sub_agent(reference)
155
+ case reference
156
+ when nil
157
+ nil
158
+ when SubAgentConfig
159
+ reference
160
+ when Hash
161
+ SubAgentConfig.from_hash(reference)
162
+ else
163
+ @sub_agents.fetch(reference.to_sym) do
164
+ raise ConfigurationError, "Unknown sub-agent: #{reference}"
165
+ end
166
+ end
167
+ end
168
+
77
169
  # Register callback for token usage events
78
170
  #
79
171
  # @yield [TokenEvent] called when tokens are used
@@ -263,6 +355,67 @@ module AgentHarness
263
355
  end
264
356
  end
265
357
 
358
+ # Provider-agnostic tool definition used during sub-agent translation.
359
+ class ToolDefinition
360
+ attr_reader :name, :description, :provider_mappings
361
+
362
+ def initialize(name:, description: nil, provider_mappings: {})
363
+ @name = name.to_sym
364
+ @description = description
365
+ @provider_mappings = deep_dup(provider_mappings).each_with_object({}) do |(provider, value), mappings|
366
+ mappings[provider.to_sym] = value
367
+ end.freeze
368
+ end
369
+
370
+ def mapping_for(provider)
371
+ deep_dup(@provider_mappings[provider.to_sym])
372
+ end
373
+
374
+ def to_h
375
+ {name: @name, description: @description, provider_mappings: deep_dup(@provider_mappings)}
376
+ end
377
+
378
+ private
379
+
380
+ def deep_dup(value)
381
+ case value
382
+ when Array
383
+ value.map { |entry| deep_dup(entry) }
384
+ when Hash
385
+ value.each_with_object({}) { |(key, entry), copy| copy[key] = deep_dup(entry) }
386
+ else
387
+ value.dup
388
+ end
389
+ rescue TypeError
390
+ value
391
+ end
392
+ end
393
+
394
+ # Registry for canonical tool definitions referenced by sub-agents.
395
+ class ToolRegistry
396
+ def initialize
397
+ @tools = {}
398
+ end
399
+
400
+ def register(name, description: nil, **provider_mappings)
401
+ @tools[name.to_sym] = ToolDefinition.new(
402
+ name: name,
403
+ description: description,
404
+ provider_mappings: provider_mappings
405
+ )
406
+ end
407
+
408
+ def fetch(name)
409
+ @tools.fetch(name.to_sym) do
410
+ raise ConfigurationError, "Unknown tool: #{name}"
411
+ end
412
+ end
413
+
414
+ def registered?(name)
415
+ @tools.key?(name.to_sym)
416
+ end
417
+ end
418
+
266
419
  # Registry for event callbacks
267
420
  class CallbackRegistry
268
421
  def initialize
@@ -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: ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
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] command to launch (stdio only)
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
- def initialize(name:, transport:, command: nil, args: nil, env: nil, url: nil)
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 validate_stdio!
141
- if @command.nil? || !@command.is_a?(Array) || @command.empty?
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
- unless @command.all? { |c| c.is_a?(String) }
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}' command must contain only strings"
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 write_mcp_config_file(mcp_servers, working_dir: nil)
760
- require "tempfile"
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)
@@ -126,6 +126,8 @@ module AgentHarness
126
126
 
127
127
  # Coerce provider_runtime from Hash if needed
128
128
  options = normalize_provider_runtime(options)
129
+ options = normalize_sub_agent(options)
130
+ prompt = apply_sub_agent_to_prompt(prompt, options[:translated_sub_agent])
129
131
 
130
132
  # Normalize and validate MCP servers
131
133
  options = normalize_mcp_servers(options)
@@ -210,6 +212,7 @@ module AgentHarness
210
212
  end
211
213
 
212
214
  options = normalize_provider_runtime(options)
215
+ options = normalize_sub_agent(options)
213
216
  runtime = options[:provider_runtime]
214
217
  conversation ||= messages
215
218
  raise ArgumentError, "conversation or messages is required" unless conversation
@@ -217,6 +220,7 @@ module AgentHarness
217
220
 
218
221
  transport = resolve_chat_transport(options)
219
222
  messages = format_messages_for_transport(conversation, transport)
223
+ messages = apply_sub_agent_to_messages(messages, options[:translated_sub_agent])
220
224
  transport_opts = chat_transport_options(runtime, options)
221
225
  transport_opts[:on_chat_chunk] = on_chat_chunk if on_chat_chunk
222
226
  transport_opts[:observer] = observer if observer
@@ -415,7 +419,13 @@ module AgentHarness
415
419
  end
416
420
 
417
421
  def normalize_mcp_servers(options)
418
- servers = options[:mcp_servers]
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
419
429
  return options if servers.nil?
420
430
 
421
431
  unless servers.is_a?(Array)
@@ -446,6 +456,33 @@ module AgentHarness
446
456
  options.merge(mcp_servers: normalized)
447
457
  end
448
458
 
459
+ def normalize_sub_agent(options)
460
+ sub_agent = options[:sub_agent]
461
+ return options unless sub_agent
462
+
463
+ resolved = AgentHarness.configuration.resolve_sub_agent(sub_agent)
464
+ translated = SubAgentTranslator.for_provider(
465
+ self.class.provider_name,
466
+ resolved,
467
+ tool_registry: AgentHarness.configuration.tool_registry,
468
+ mcp_servers: AgentHarness.configuration.mcp_servers
469
+ )
470
+
471
+ options.merge(sub_agent: resolved, translated_sub_agent: translated)
472
+ end
473
+
474
+ def apply_sub_agent_to_prompt(prompt, translated_sub_agent)
475
+ return prompt unless translated_sub_agent
476
+
477
+ [translated_sub_agent[:runtime_instructions], "User task:\n#{prompt}"].join("\n\n")
478
+ end
479
+
480
+ def apply_sub_agent_to_messages(messages, translated_sub_agent)
481
+ return messages unless translated_sub_agent
482
+
483
+ [{role: "system", content: translated_sub_agent[:runtime_instructions]}] + messages
484
+ end
485
+
449
486
  def command_execution_options(options)
450
487
  execution_options = {
451
488
  idle_timeout: options[:idle_timeout],
@@ -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: false,
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
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentHarness
4
+ # Canonical provider-agnostic sub-agent definition.
5
+ class SubAgentConfig
6
+ attr_reader :name, :description, :instructions, :model, :tools, :mcp_servers
7
+ attr_reader :constraints, :handoff_conditions, :type, :sub_agents, :routing
8
+
9
+ def initialize(name:, description:, instructions:, model: "default", tools: [],
10
+ mcp_servers: [], constraints: {}, handoff_conditions: [], type: nil,
11
+ sub_agents: [], routing: nil)
12
+ @name = normalize_name(name)
13
+ @description = validate_string!(:description, description)
14
+ @instructions = validate_string!(:instructions, instructions)
15
+ @model = normalize_model(model)
16
+ @tools = normalize_array(:tools, tools)
17
+ @mcp_servers = normalize_array(:mcp_servers, mcp_servers)
18
+ @constraints = normalize_hash(:constraints, constraints)
19
+ @handoff_conditions = normalize_array(:handoff_conditions, handoff_conditions)
20
+ @type = type&.to_sym
21
+ @sub_agents = normalize_array(:sub_agents, sub_agents)
22
+ @routing = routing.nil? ? nil : normalize_hash(:routing, routing)
23
+ end
24
+
25
+ def self.from_hash(hash)
26
+ unless hash.is_a?(Hash)
27
+ raise ConfigurationError, "Sub-agent definition must be a Hash, got #{hash.class}"
28
+ end
29
+
30
+ attrs = hash.each_with_object({}) do |(key, value), memo|
31
+ memo[key.to_sym] = value
32
+ end
33
+
34
+ %i[name description instructions].each do |field|
35
+ value = attrs[field]
36
+ next if value.is_a?(String) && !value.strip.empty?
37
+ next if value.is_a?(Symbol)
38
+
39
+ raise ConfigurationError, "#{field} is required"
40
+ end
41
+
42
+ new(**attrs)
43
+ end
44
+
45
+ def to_h
46
+ {
47
+ name: @name,
48
+ description: @description,
49
+ instructions: @instructions,
50
+ model: @model,
51
+ tools: deep_dup(@tools),
52
+ mcp_servers: deep_dup(@mcp_servers),
53
+ constraints: deep_dup(@constraints),
54
+ handoff_conditions: deep_dup(@handoff_conditions),
55
+ type: @type,
56
+ sub_agents: deep_dup(@sub_agents),
57
+ routing: deep_dup(@routing)
58
+ }.compact
59
+ end
60
+
61
+ private
62
+
63
+ def normalize_name(name)
64
+ value = validate_string!(:name, name)
65
+ value.tr(" ", "_").to_sym
66
+ end
67
+
68
+ def normalize_model(model)
69
+ return "default" if model.nil?
70
+
71
+ validate_string!(:model, model)
72
+ end
73
+
74
+ def validate_string!(field, value)
75
+ unless value.is_a?(String) || value.is_a?(Symbol)
76
+ raise ConfigurationError, "#{field} must be a String or Symbol"
77
+ end
78
+
79
+ string = value.to_s.strip
80
+ raise ConfigurationError, "#{field} is required" if string.empty?
81
+
82
+ string
83
+ end
84
+
85
+ def normalize_array(field, value)
86
+ return [].freeze if value.nil?
87
+
88
+ unless value.is_a?(Array)
89
+ raise ConfigurationError, "#{field} must be an Array"
90
+ end
91
+
92
+ deep_dup(value).freeze
93
+ end
94
+
95
+ def normalize_hash(field, value)
96
+ return {}.freeze if value.nil?
97
+
98
+ unless value.is_a?(Hash)
99
+ raise ConfigurationError, "#{field} must be a Hash"
100
+ end
101
+
102
+ deep_dup(value).freeze
103
+ end
104
+
105
+ def deep_dup(value)
106
+ case value
107
+ when Array
108
+ value.map { |entry| deep_dup(entry) }
109
+ when Hash
110
+ value.each_with_object({}) { |(key, entry), copy| copy[key] = deep_dup(entry) }
111
+ else
112
+ value.dup
113
+ end
114
+ rescue TypeError
115
+ value
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module AgentHarness
6
+ # Loads canonical sub-agent definitions from YAML or Markdown files.
7
+ class SubAgentFileLoader
8
+ class << self
9
+ def load(path)
10
+ case File.extname(path).downcase
11
+ when ".yml", ".yaml"
12
+ load_yaml(path)
13
+ when ".md", ".markdown"
14
+ [load_markdown(path)]
15
+ else
16
+ raise ConfigurationError, "Unsupported sub-agent definition format: #{path}"
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def load_yaml(path)
23
+ parsed = YAML.safe_load_file(path, permitted_classes: [], aliases: false)
24
+ unless parsed.is_a?(Hash)
25
+ raise ConfigurationError, "YAML sub-agent definition must be a Hash"
26
+ end
27
+
28
+ agents = parsed["agents"] || parsed[:agents] || [parsed]
29
+ unless agents.is_a?(Array)
30
+ raise ConfigurationError, "YAML sub-agent definitions must provide an agents Array"
31
+ end
32
+
33
+ agents.map { |entry| SubAgentConfig.from_hash(entry) }
34
+ rescue Psych::SyntaxError => e
35
+ raise ConfigurationError, "Invalid YAML in #{path}: #{e.message}"
36
+ end
37
+
38
+ def load_markdown(path)
39
+ content = File.read(path)
40
+ match = content.match(/\A---\s*\n(?<frontmatter>.*?)\n---\s*\n?(?<body>.*)\z/m)
41
+ raise ConfigurationError, "Markdown sub-agent definitions require YAML frontmatter" unless match
42
+
43
+ attrs = YAML.safe_load(match[:frontmatter], permitted_classes: [], aliases: false) || {}
44
+ unless attrs.is_a?(Hash)
45
+ raise ConfigurationError, "Markdown frontmatter must be a Hash"
46
+ end
47
+
48
+ attrs["instructions"] ||= match[:body].strip
49
+ SubAgentConfig.from_hash(attrs)
50
+ rescue Psych::SyntaxError => e
51
+ raise ConfigurationError, "Invalid frontmatter in #{path}: #{e.message}"
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module AgentHarness
6
+ # Translates canonical sub-agent definitions into provider-specific formats.
7
+ module SubAgentTranslator
8
+ class << self
9
+ def for_provider(provider, sub_agent_config, tool_registry: AgentHarness.configuration.tool_registry,
10
+ mcp_servers: AgentHarness.configuration.mcp_servers)
11
+ config = normalize_sub_agent_config(sub_agent_config)
12
+ normalized_provider = normalize_provider(provider)
13
+ tools = resolve_tools(config.tools, provider: normalized_provider, tool_registry: tool_registry)
14
+ servers = resolve_mcp_servers(config.mcp_servers, mcp_servers: mcp_servers)
15
+
16
+ case normalized_provider
17
+ when :anthropic
18
+ translate_for_anthropic(config, tools: tools, mcp_servers: servers)
19
+ when :openai
20
+ translate_for_openai(config, tools: tools, mcp_servers: servers)
21
+ when :google
22
+ translate_for_google(config, tools: tools, mcp_servers: servers)
23
+ when :claude_code
24
+ translate_for_claude_code(config, tools: tools, mcp_servers: servers)
25
+ when :codex
26
+ translate_for_codex(config, tools: tools, mcp_servers: servers)
27
+ when :pi
28
+ translate_for_pi(config, tools: tools, mcp_servers: servers)
29
+ else
30
+ translate_generic(normalized_provider, config, tools: tools, mcp_servers: servers)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def normalize_sub_agent_config(sub_agent_config)
37
+ case sub_agent_config
38
+ when SubAgentConfig
39
+ sub_agent_config
40
+ when Hash
41
+ SubAgentConfig.from_hash(sub_agent_config)
42
+ else
43
+ raise ConfigurationError, "Sub-agent config must be a SubAgentConfig or Hash"
44
+ end
45
+ end
46
+
47
+ def normalize_provider(provider)
48
+ case provider.to_sym
49
+ when :claude then :anthropic
50
+ when :gemini then :google
51
+ else provider.to_sym
52
+ end
53
+ end
54
+
55
+ def resolve_tools(tool_refs, provider:, tool_registry:)
56
+ tool_refs.map do |tool|
57
+ case tool
58
+ when Symbol, String
59
+ mapping = tool_registry.fetch(tool).mapping_for(provider)
60
+ mapping.nil? ? tool.to_s : mapping
61
+ when Hash
62
+ deep_dup(tool)
63
+ else
64
+ raise ConfigurationError, "Unsupported tool reference #{tool.inspect} in sub-agent definition"
65
+ end
66
+ end
67
+ end
68
+
69
+ def resolve_mcp_servers(server_refs, mcp_servers:)
70
+ server_refs.map do |server|
71
+ resolved = case server
72
+ when McpServer
73
+ server
74
+ when Symbol, String
75
+ mcp_servers.fetch(server.to_sym) do
76
+ raise ConfigurationError, "Unknown MCP server: #{server}"
77
+ end
78
+ when Hash
79
+ McpServer.from_hash(server)
80
+ else
81
+ raise ConfigurationError, "Unsupported MCP server reference #{server.inspect} in sub-agent definition"
82
+ end
83
+
84
+ resolved.to_h
85
+ end
86
+ end
87
+
88
+ def translate_for_anthropic(config, tools:, mcp_servers:)
89
+ {
90
+ provider: :anthropic,
91
+ format: :agent_sdk,
92
+ agent: {
93
+ name: config.name.to_s,
94
+ description: config.description,
95
+ instructions: config.instructions,
96
+ model: config.model,
97
+ tools: tools,
98
+ mcp_servers: mcp_servers,
99
+ constraints: deep_dup(config.constraints)
100
+ },
101
+ runtime_instructions: runtime_instructions(config)
102
+ }
103
+ end
104
+
105
+ def translate_for_openai(config, tools:, mcp_servers:)
106
+ {
107
+ provider: :openai,
108
+ format: :responses,
109
+ assistant: {
110
+ name: config.name.to_s,
111
+ description: config.description,
112
+ instructions: config.instructions,
113
+ model: config.model,
114
+ tools: tools,
115
+ metadata: {
116
+ mcp_servers: mcp_servers,
117
+ constraints: deep_dup(config.constraints)
118
+ }
119
+ },
120
+ runtime_instructions: runtime_instructions(config)
121
+ }
122
+ end
123
+
124
+ def translate_for_google(config, tools:, mcp_servers:)
125
+ {
126
+ provider: :google,
127
+ format: :adk,
128
+ agent: {
129
+ name: config.name.to_s,
130
+ description: config.description,
131
+ instruction: config.instructions,
132
+ model: config.model,
133
+ tools: tools,
134
+ mcp_servers: mcp_servers,
135
+ constraints: deep_dup(config.constraints)
136
+ },
137
+ runtime_instructions: runtime_instructions(config)
138
+ }
139
+ end
140
+
141
+ def translate_for_claude_code(config, tools:, mcp_servers:)
142
+ frontmatter = {
143
+ "name" => config.name.to_s,
144
+ "description" => config.description,
145
+ "model" => config.model,
146
+ "tools" => tools,
147
+ "mcp_servers" => mcp_servers,
148
+ "constraints" => deep_dup(config.constraints)
149
+ }.delete_if { |_key, value| value.respond_to?(:empty?) ? value.empty? : value.nil? }
150
+
151
+ {
152
+ provider: :claude_code,
153
+ format: :markdown,
154
+ content: build_markdown_definition(frontmatter, config.instructions),
155
+ runtime_instructions: runtime_instructions(config)
156
+ }
157
+ end
158
+
159
+ def translate_for_codex(config, tools:, mcp_servers:)
160
+ {
161
+ provider: :codex,
162
+ format: :delegated_prompt,
163
+ definition: {
164
+ name: config.name.to_s,
165
+ description: config.description,
166
+ instructions: config.instructions,
167
+ model: config.model,
168
+ tools: tools,
169
+ mcp_servers: mcp_servers,
170
+ constraints: deep_dup(config.constraints)
171
+ },
172
+ runtime_instructions: runtime_instructions(config)
173
+ }
174
+ end
175
+
176
+ def translate_for_pi(config, tools:, mcp_servers:)
177
+ frontmatter = {
178
+ "name" => config.name.to_s,
179
+ "description" => config.description,
180
+ "model" => config.model,
181
+ "tools" => tools,
182
+ "mcp_servers" => mcp_servers
183
+ }.delete_if { |_key, value| value.respond_to?(:empty?) ? value.empty? : value.nil? }
184
+
185
+ {
186
+ provider: :pi,
187
+ format: :skill_markdown,
188
+ content: build_markdown_definition(frontmatter, config.instructions),
189
+ runtime_instructions: runtime_instructions(config)
190
+ }
191
+ end
192
+
193
+ def translate_generic(provider, config, tools:, mcp_servers:)
194
+ {
195
+ provider: provider,
196
+ format: :generic,
197
+ definition: {
198
+ name: config.name.to_s,
199
+ description: config.description,
200
+ instructions: config.instructions,
201
+ model: config.model,
202
+ tools: tools,
203
+ mcp_servers: mcp_servers,
204
+ constraints: deep_dup(config.constraints)
205
+ },
206
+ runtime_instructions: runtime_instructions(config)
207
+ }
208
+ end
209
+
210
+ def runtime_instructions(config)
211
+ <<~TEXT.strip
212
+ Sub-agent role: #{config.name}
213
+ Description: #{config.description}
214
+
215
+ Follow these sub-agent instructions exactly:
216
+ #{config.instructions}
217
+ TEXT
218
+ end
219
+
220
+ def build_markdown_definition(frontmatter, instructions)
221
+ <<~MARKDOWN
222
+ ---
223
+ #{YAML.dump(frontmatter).sub(/\A---\s*\n/, "").strip}
224
+ ---
225
+ #{instructions}
226
+ MARKDOWN
227
+ end
228
+
229
+ def deep_dup(value)
230
+ case value
231
+ when Array
232
+ value.map { |entry| deep_dup(entry) }
233
+ when Hash
234
+ value.each_with_object({}) { |(key, entry), copy| copy[key] = deep_dup(entry) }
235
+ else
236
+ value.dup
237
+ end
238
+ rescue TypeError
239
+ value
240
+ end
241
+ end
242
+ end
243
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentHarness
4
- VERSION = "0.11.0"
4
+ VERSION = "0.11.2"
5
5
  end
data/lib/agent_harness.rb CHANGED
@@ -77,6 +77,23 @@ module AgentHarness
77
77
  conductor.send_message(prompt, provider: provider, executor: executor, **options)
78
78
  end
79
79
 
80
+ # Resolve a canonical sub-agent definition by name or inline payload.
81
+ #
82
+ # @param reference [Symbol, String, Hash, SubAgentConfig]
83
+ # @return [SubAgentConfig]
84
+ def sub_agent(reference)
85
+ configuration.resolve_sub_agent(reference)
86
+ end
87
+
88
+ # Translate a canonical sub-agent definition into a provider-specific format.
89
+ #
90
+ # @param reference [Symbol, String, Hash, SubAgentConfig] sub-agent reference
91
+ # @param provider [Symbol, String] target provider
92
+ # @return [Hash] provider-specific sub-agent definition
93
+ def translate_sub_agent(reference, provider:)
94
+ SubAgentTranslator.for_provider(provider, sub_agent(reference))
95
+ end
96
+
80
97
  # Get a provider instance
81
98
  # @param name [Symbol] the provider name
82
99
  # @return [Providers::Base] the provider instance
@@ -261,6 +278,11 @@ end
261
278
  # Core components
262
279
  require_relative "agent_harness/errors"
263
280
  require_relative "agent_harness/mcp_server"
281
+ require_relative "agent_harness/mcp_config_loader"
282
+ require_relative "agent_harness/mcp_config_translator"
283
+ require_relative "agent_harness/sub_agent_config"
284
+ require_relative "agent_harness/sub_agent_file_loader"
285
+ require_relative "agent_harness/sub_agent_translator"
264
286
  require_relative "agent_harness/provider_runtime"
265
287
  require_relative "agent_harness/execution_preparation"
266
288
  require_relative "agent_harness/configuration"
@@ -281,6 +303,7 @@ require_relative "agent_harness/providers/adapter"
281
303
  require_relative "agent_harness/providers/base"
282
304
  require_relative "agent_harness/providers/token_usage_parsing"
283
305
  require_relative "agent_harness/providers/rate_limit_reset_parsing"
306
+ require_relative "agent_harness/providers/mcp_config_file_support"
284
307
  require_relative "agent_harness/providers/anthropic"
285
308
  require_relative "agent_harness/providers/aider"
286
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.0
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,12 +128,16 @@ 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
132
135
  - lib/agent_harness/providers/registry.rb
133
136
  - lib/agent_harness/providers/token_usage_parsing.rb
134
137
  - lib/agent_harness/response.rb
138
+ - lib/agent_harness/sub_agent_config.rb
139
+ - lib/agent_harness/sub_agent_file_loader.rb
140
+ - lib/agent_harness/sub_agent_translator.rb
135
141
  - lib/agent_harness/text_transport.rb
136
142
  - lib/agent_harness/token_tracker.rb
137
143
  - lib/agent_harness/version.rb