kward 0.73.1 → 0.74.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.
@@ -88,10 +88,26 @@ module Kward
88
88
  visible = line.to_s[column_offset.to_i, text_width].to_s
89
89
  rendered = editor_render_visible_line(visible, line_index)
90
90
  line_start = @editor_state.line_start_offset(line_index)
91
+ rendered = editor_overlay_search_matches(rendered, line_start, column_offset, visible.length)
91
92
  rendered = editor_overlay_line_selections(rendered, line_start, column_offset, visible.length)
92
93
  editor_overlay_secondary_cursors(rendered, line_start, column_offset, visible.length, text_width)
93
94
  end
94
95
 
96
+ def editor_overlay_search_matches(rendered, line_start, column_offset, visible_length)
97
+ ranges = @editor_state.search_match_ranges
98
+ return rendered if ranges.empty?
99
+
100
+ visible_offset = line_start + column_offset.to_i
101
+ match_ranges = ranges.filter_map do |range|
102
+ match_start = [range[0] - visible_offset, 0].max
103
+ match_end = [range[1] - visible_offset, visible_length].min
104
+ [match_start, match_end] if match_start < match_end
105
+ end
106
+ return rendered if match_ranges.empty?
107
+
108
+ editor_overlay_selection(rendered, match_ranges)
109
+ end
110
+
95
111
  def editor_overlay_line_selections(rendered, line_start, column_offset, visible_length)
96
112
  ranges = @editor_state.selection_ranges
97
113
  return rendered if ranges.empty?
@@ -6,44 +6,82 @@ module Kward
6
6
  class EditorSearch
7
7
  attr_reader :query, :direction
8
8
 
9
+ def self.match_ranges(text, query, base_offset: 0)
10
+ query = query.to_s
11
+ return [] if query.empty?
12
+
13
+ haystack, needle = normalized_pair(text.to_s, query)
14
+ ranges = []
15
+ start = 0
16
+ while (index = haystack.index(needle, start))
17
+ ranges << [base_offset + index, base_offset + index + query.length]
18
+ start = index + [query.length, 1].max
19
+ end
20
+ ranges
21
+ end
22
+
23
+ def self.normalized_pair(buffer, query)
24
+ return [buffer, query] if case_sensitive?(query)
25
+
26
+ [buffer.downcase, query.downcase]
27
+ end
28
+
29
+ def self.case_sensitive?(query)
30
+ query.to_s.match?(/[[:upper:]]/)
31
+ end
32
+
9
33
  def initialize(direction: :forward)
10
34
  @active = false
11
35
  @query = +""
12
36
  @direction = direction
37
+ @origin_cursor = nil
38
+ @current_match = nil
13
39
  end
14
40
 
15
41
  def active?
16
42
  @active == true
17
43
  end
18
44
 
19
- def begin(direction = :forward)
45
+ def begin(direction = :forward, cursor: nil)
20
46
  @active = true
21
47
  @direction = direction
22
48
  @query = +""
49
+ @origin_cursor = cursor
50
+ @current_match = nil
23
51
  status_prefix
24
52
  end
25
53
 
26
- def cancel
54
+ def cancel(restore_cursor: true)
27
55
  @active = false
28
- "Search cancelled"
56
+ cursor = @origin_cursor if restore_cursor
57
+ @origin_cursor = nil
58
+ @current_match = nil
59
+ { cursor: cursor, status: "Search cancelled", found: false }
29
60
  end
30
61
 
31
- def append(text)
62
+ def append(text, buffer:, cursor:)
32
63
  @query << text.to_s
33
- "#{status_prefix} #{@query}"
64
+ live_result(buffer: buffer, cursor: cursor)
34
65
  end
35
66
 
36
- def delete_character
67
+ def delete_character(buffer:, cursor:)
37
68
  @query = @query[0...-1].to_s
38
- "#{status_prefix} #{@query}"
69
+ live_result(buffer: buffer, cursor: cursor)
39
70
  end
40
71
 
41
72
  def confirm(buffer:, cursor:)
42
73
  confirmed_query = @query.to_s
43
74
  @active = false
75
+ @origin_cursor = nil
44
76
  return { status: "Search cancelled", found: false } if confirmed_query.empty?
45
77
 
78
+ if @current_match
79
+ return { cursor: @current_match, status: "Found: #{confirmed_query}", found: true }
80
+ end
81
+
46
82
  repeat(buffer: buffer, cursor: cursor, direction: @direction, query: confirmed_query)
83
+ ensure
84
+ @current_match = nil
47
85
  end
48
86
 
49
87
  def repeat(buffer:, cursor:, direction: @direction, query: @query)
@@ -52,12 +90,7 @@ module Kward
52
90
 
53
91
  @query = query
54
92
  @direction = direction
55
- index = if direction == :backward
56
- search_from = cursor.positive? ? cursor - 1 : buffer.length
57
- buffer.rindex(query, search_from) || buffer.rindex(query)
58
- else
59
- buffer.index(query, cursor + 1) || buffer.index(query)
60
- end
93
+ index = find_match(buffer, query, cursor, direction)
61
94
 
62
95
  if index
63
96
  { cursor: index, status: "Found: #{query}", found: true }
@@ -68,6 +101,30 @@ module Kward
68
101
 
69
102
  private
70
103
 
104
+ def live_result(buffer:, cursor:)
105
+ return { cursor: @origin_cursor, status: status_prefix, found: false } if @query.empty?
106
+
107
+ @origin_cursor = cursor if @origin_cursor.nil?
108
+ index = find_match(buffer, @query, @origin_cursor, @direction)
109
+ @current_match = index
110
+ if index
111
+ { cursor: index, status: "#{status_prefix} #{@query}", found: true }
112
+ else
113
+ { cursor: @origin_cursor, status: "No match: #{@query}", found: false }
114
+ end
115
+ end
116
+
117
+ def find_match(buffer, query, cursor, direction)
118
+ haystack, needle = self.class.normalized_pair(buffer.to_s, query.to_s)
119
+ cursor = cursor.to_i
120
+ if direction == :backward
121
+ search_from = cursor.positive? ? cursor - 1 : haystack.length
122
+ haystack.rindex(needle, search_from) || haystack.rindex(needle)
123
+ else
124
+ haystack.index(needle, cursor + 1) || haystack.index(needle)
125
+ end
126
+ end
127
+
71
128
  def status_prefix
72
129
  @direction == :backward ? "Search backward:" : "Search:"
73
130
  end
@@ -19,6 +19,7 @@ module Kward
19
19
  attr_reader :path, :display_path, :language, :original_content, :original_digest, :original_mtime, :original_size
20
20
  attr_reader :buffer, :undo_stack, :redo_stack, :kill_buffer, :kill_ring, :last_yank_range, :last_yank_index
21
21
  attr_accessor :viewport_row, :viewport_column, :status, :overwrite_confirmed, :quit_confirmed, :search_active, :search_query, :search_direction, :new_file, :editor_mode, :emacs_pending, :readonly, :diff_view
22
+ attr_reader :search_match_ranges
22
23
 
23
24
  def initialize(path:, content:, new_file: false, editor_mode: "modern", readonly: false, diff_view: false, virtual: false, display_path: nil, language: nil)
24
25
  @path = virtual ? nil : path.to_s
@@ -45,6 +46,7 @@ module Kward
45
46
  @search_active = @search.active?
46
47
  @search_query = @search.query
47
48
  @search_direction = @search.direction
49
+ @search_match_ranges = []
48
50
  @kill_state = EditorKillRing.new
49
51
  @kill_buffer = @kill_state.kill_buffer
50
52
  @selections = EditorSelections.new(cursor: @cursor, buffer_length: @buffer.length)
@@ -80,6 +82,7 @@ module Kward
80
82
  @search_active = other.search_active
81
83
  @search_query = other.search_query.dup
82
84
  @search_direction = other.search_direction
85
+ @search_match_ranges = other.search_match_ranges.map(&:dup)
83
86
  @kill_state = EditorKillRing.new(
84
87
  kill_buffer: other.kill_buffer.dup,
85
88
  kill_ring: other.kill_ring.map(&:dup),
@@ -231,6 +234,51 @@ module Kward
231
234
  sync_vibe_state
232
235
  end
233
236
 
237
+ def vibe_register_types
238
+ @vibe_state.register_types
239
+ end
240
+
241
+ def vibe_register_types=(value)
242
+ @vibe_state.register_types = value
243
+ sync_vibe_state
244
+ end
245
+
246
+ def vibe_kill_linewise
247
+ @vibe_state.kill_linewise
248
+ end
249
+
250
+ def vibe_kill_linewise=(value)
251
+ @vibe_state.kill_linewise = value
252
+ sync_vibe_state
253
+ end
254
+
255
+ def vibe_previous_change_cursor
256
+ @vibe_state.previous_change_cursor
257
+ end
258
+
259
+ def vibe_previous_change_cursor=(value)
260
+ @vibe_state.previous_change_cursor = value
261
+ sync_vibe_state
262
+ end
263
+
264
+ def vibe_jump_back_list
265
+ @vibe_state.jump_back_list
266
+ end
267
+
268
+ def vibe_jump_back_list=(value)
269
+ @vibe_state.jump_back_list = value
270
+ sync_vibe_state
271
+ end
272
+
273
+ def vibe_jump_forward_list
274
+ @vibe_state.jump_forward_list
275
+ end
276
+
277
+ def vibe_jump_forward_list=(value)
278
+ @vibe_state.jump_forward_list = value
279
+ sync_vibe_state
280
+ end
281
+
234
282
  def vibe_macros
235
283
  @vibe_state.macros
236
284
  end
@@ -372,7 +420,7 @@ module Kward
372
420
  end
373
421
 
374
422
  restore_editor_snapshot(snapshot)
375
- changed!(clear_selections: false)
423
+ changed!
376
424
  @status = "Undo"
377
425
  true
378
426
  end
@@ -385,7 +433,7 @@ module Kward
385
433
  end
386
434
 
387
435
  restore_editor_snapshot(snapshot)
388
- changed!(clear_selections: false)
436
+ changed!
389
437
  @status = "Redo"
390
438
  true
391
439
  end
@@ -909,27 +957,28 @@ module Kward
909
957
  end
910
958
 
911
959
  def begin_search(direction = :forward)
912
- @status = @search.begin(direction)
960
+ @status = @search.begin(direction, cursor: @cursor)
913
961
  sync_search_state
914
962
  true
915
963
  end
916
964
 
917
- def cancel_search
918
- @status = @search.cancel
919
- sync_search_state
965
+ def cancel_search(restore_cursor: true)
966
+ apply_search_result(@search.cancel(restore_cursor: restore_cursor))
967
+ @search_match_ranges = []
920
968
  true
921
969
  end
922
970
 
923
- def append_search(text)
924
- @status = @search.append(text)
925
- sync_search_state
971
+ def clear_search_highlights
972
+ @search_match_ranges = []
926
973
  true
927
974
  end
928
975
 
976
+ def append_search(text)
977
+ apply_search_result(@search.append(text, buffer: @buffer, cursor: @cursor))
978
+ end
979
+
929
980
  def delete_search_character
930
- @status = @search.delete_character
931
- sync_search_state
932
- true
981
+ apply_search_result(@search.delete_character(buffer: @buffer, cursor: @cursor))
933
982
  end
934
983
 
935
984
  def confirm_search
@@ -1224,6 +1273,7 @@ module Kward
1224
1273
  @search_active = @search.active?
1225
1274
  @search_query = @search.query
1226
1275
  @search_direction = @search.direction
1276
+ @search_match_ranges = EditorSearch.match_ranges(@buffer, @search_query)
1227
1277
  end
1228
1278
 
1229
1279
  def sync_vibe_state
@@ -1236,6 +1286,11 @@ module Kward
1236
1286
  @vibe_visual_block_insert = @vibe_state.visual_block_insert
1237
1287
  @vibe_marks = @vibe_state.marks
1238
1288
  @vibe_registers = @vibe_state.registers
1289
+ @vibe_register_types = @vibe_state.register_types
1290
+ @vibe_kill_linewise = @vibe_state.kill_linewise
1291
+ @vibe_previous_change_cursor = @vibe_state.previous_change_cursor
1292
+ @vibe_jump_back_list = @vibe_state.jump_back_list
1293
+ @vibe_jump_forward_list = @vibe_state.jump_forward_list
1239
1294
  @vibe_macros = @vibe_state.macros
1240
1295
  @vibe_recording_macro = @vibe_state.recording_macro
1241
1296
  @vibe_last_macro = @vibe_state.last_macro
@@ -6,7 +6,8 @@ module Kward
6
6
  class VibeEditorState
7
7
  attr_accessor :mode, :pending, :command, :last_change, :last_find
8
8
  attr_accessor :last_visual_selection, :visual_block_insert
9
- attr_accessor :marks, :registers, :macros, :recording_macro, :last_macro
9
+ attr_accessor :marks, :registers, :register_types, :macros, :recording_macro, :last_macro
10
+ attr_accessor :kill_linewise, :previous_change_cursor, :jump_back_list, :jump_forward_list
10
11
 
11
12
  def initialize(editor_mode: "modern")
12
13
  @mode = editor_mode == "vibe" ? "normal" : nil
@@ -18,6 +19,11 @@ module Kward
18
19
  @visual_block_insert = nil
19
20
  @marks = {}
20
21
  @registers = {}
22
+ @register_types = {}
23
+ @kill_linewise = false
24
+ @previous_change_cursor = nil
25
+ @jump_back_list = []
26
+ @jump_forward_list = []
21
27
  @macros = {}
22
28
  @recording_macro = nil
23
29
  @last_macro = nil
@@ -34,6 +40,11 @@ module Kward
34
40
  state.visual_block_insert = other.visual_block_insert&.dup
35
41
  state.marks = other.marks.transform_values(&:dup)
36
42
  state.registers = other.registers.transform_values(&:dup)
43
+ state.register_types = other.register_types.transform_values(&:dup)
44
+ state.kill_linewise = other.kill_linewise
45
+ state.previous_change_cursor = other.previous_change_cursor
46
+ state.jump_back_list = other.jump_back_list.map(&:dup)
47
+ state.jump_forward_list = other.jump_forward_list.map(&:dup)
37
48
  state.macros = other.macros.transform_values(&:dup)
38
49
  state.recording_macro = other.recording_macro
39
50
  state.last_macro = other.last_macro
@@ -434,6 +434,7 @@ module Kward
434
434
  memory: { supported: true, optIn: true, defaultEnabled: false, autoSummaryDefaultEnabled: false, promptInjection: "interactive", storage: { core: "json", soft: "jsonl", events: "jsonl" }, methods: MEMORY_METHODS },
435
435
  workers: workers_capability,
436
436
  commands: { supported: true, methods: COMMAND_METHODS, method: COMMAND_METHODS[0], runMethod: COMMAND_METHODS[1], sources: ["builtin", "prompt", "skill", "plugin"], executableSources: ["builtin", "plugin"] },
437
+ mcp: { supported: true, transport: "stdio", config: "mcpServers", exposes: ["tools"], unsupported: ["resources", "prompts", "sampling", "streamableHttp"] },
437
438
  startupResources: { supported: true, method: STARTUP_RESOURCE_METHODS.first },
438
439
  starterPack: { supported: false, reason: "cliOnlyInstallCommand" },
439
440
  shell: { supported: false, reason: "interactiveTuiOnly" },
@@ -0,0 +1,118 @@
1
+ require "json"
2
+ require_relative "base"
3
+
4
+ # Namespace for the Kward CLI agent runtime.
5
+ module Kward
6
+ module Tools
7
+ # Adapts an MCP server tool to Kward's model-callable tool interface.
8
+ class MCPTool < Base
9
+ attr_reader :server_name, :remote_name
10
+
11
+ def initialize(server_name:, client:, tool:)
12
+ @server_name = server_name.to_s
13
+ @client = client
14
+ @tool = tool
15
+ @remote_name = value(tool, "name").to_s
16
+ super(kward_name(@server_name, @remote_name), description, properties: {}, required: [])
17
+ end
18
+
19
+ def schema
20
+ {
21
+ type: "function",
22
+ function: {
23
+ name: name,
24
+ description: description,
25
+ parameters: input_schema
26
+ }
27
+ }
28
+ end
29
+
30
+ def call(args, _conversation, cancellation: nil)
31
+ cancellation&.raise_if_cancelled!
32
+ result = @client.call_tool(@remote_name, args || {})
33
+ format_result(result)
34
+ rescue StandardError => e
35
+ "MCP tool #{server_name}.#{remote_name} failed: #{e.message}"
36
+ end
37
+
38
+ def self.kward_name(server_name, tool_name)
39
+ "#{sanitize_name(server_name)}__#{sanitize_name(tool_name)}"
40
+ end
41
+
42
+ def self.sanitize_name(value)
43
+ text = value.to_s.gsub(/[^A-Za-z0-9_-]/, "_")
44
+ text = "mcp" if text.empty?
45
+ text[0, 60]
46
+ end
47
+
48
+ private
49
+
50
+ def kward_name(server_name, tool_name)
51
+ self.class.kward_name(server_name, tool_name)
52
+ end
53
+
54
+ def description
55
+ value(@tool, "description").to_s.empty? ? "MCP tool #{@server_name}.#{@remote_name}" : value(@tool, "description").to_s
56
+ end
57
+
58
+ def input_schema
59
+ schema = value(@tool, "inputSchema")
60
+ return empty_object_schema unless schema.is_a?(Hash)
61
+
62
+ schema.empty? ? empty_object_schema : schema
63
+ end
64
+
65
+ def empty_object_schema
66
+ { type: "object", properties: {}, additionalProperties: false }
67
+ end
68
+
69
+ def format_result(result)
70
+ return "MCP tool #{server_name}.#{remote_name} returned no content." unless result.is_a?(Hash)
71
+
72
+ parts = []
73
+ parts << "MCP tool #{server_name}.#{remote_name} reported an error:" if truthy?(value(result, "isError"))
74
+ parts.concat(format_content(value(result, "content")))
75
+
76
+ structured = value(result, "structuredContent")
77
+ parts << JSON.pretty_generate(structured) unless structured.nil?
78
+
79
+ parts.empty? ? "MCP tool #{server_name}.#{remote_name} returned no content." : parts.join("\n\n")
80
+ end
81
+
82
+ def format_content(content)
83
+ Array(content).filter_map do |entry|
84
+ next entry.to_s unless entry.is_a?(Hash)
85
+
86
+ case value(entry, "type")
87
+ when "text"
88
+ value(entry, "text").to_s
89
+ when "image"
90
+ "[MCP image content: #{value(entry, "mimeType") || "unknown MIME type"}]"
91
+ when "audio"
92
+ "[MCP audio content: #{value(entry, "mimeType") || "unknown MIME type"}]"
93
+ when "resource_link"
94
+ "[MCP resource link: #{value(entry, "uri") || value(entry, "name") || "unnamed"}]"
95
+ when "resource"
96
+ "[MCP embedded resource: #{resource_label(value(entry, "resource"))}]"
97
+ else
98
+ JSON.generate(entry)
99
+ end
100
+ end
101
+ end
102
+
103
+ def resource_label(resource)
104
+ return "unnamed" unless resource.is_a?(Hash)
105
+
106
+ value(resource, "uri") || value(resource, "name") || "unnamed"
107
+ end
108
+
109
+ def truthy?(value)
110
+ value == true || value.to_s == "true"
111
+ end
112
+
113
+ def value(object, key)
114
+ object[key] || object[key.to_sym]
115
+ end
116
+ end
117
+ end
118
+ end
@@ -8,6 +8,7 @@ require_relative "edit_file"
8
8
  require_relative "fetch_content"
9
9
  require_relative "fetch_raw"
10
10
  require_relative "list_directory"
11
+ require_relative "mcp_tool"
11
12
  require_relative "read_file"
12
13
  require_relative "read_skill"
13
14
  require_relative "run_shell_command"
@@ -19,6 +20,7 @@ require_relative "search/code"
19
20
  require_relative "search/web"
20
21
  require_relative "search/web_fetch"
21
22
  require_relative "tool_call"
23
+ require_relative "../mcp/server_config"
22
24
  require_relative "../telemetry/logger"
23
25
  require_relative "../tool_output_compactor"
24
26
  require_relative "../workspace"
@@ -60,7 +62,7 @@ module Kward
60
62
  # @param web_search_enabled [Boolean, nil] override for web search exposure
61
63
  # @param skills [Array<ConfigFiles::Skill>, nil] override discovered skills
62
64
  # @param ask_user_question_enabled [Boolean, nil] override question exposure
63
- def initialize(workspace: Workspace.new, prompt: nil, web_search: WebSearch.new, web_fetch: WebFetch.new, code_search: CodeSearch.new, web_search_enabled: nil, skills: nil, ask_user_question_enabled: nil, allowed_tool_names: nil, write_lock: nil, writer_id: nil, tool_output_compactor: ToolOutputCompactor.new, telemetry_logger: TelemetryLogger.new, context_budget_meter: nil)
65
+ def initialize(workspace: Workspace.new, prompt: nil, web_search: WebSearch.new, web_fetch: WebFetch.new, code_search: CodeSearch.new, web_search_enabled: nil, skills: nil, ask_user_question_enabled: nil, allowed_tool_names: nil, write_lock: nil, writer_id: nil, tool_output_compactor: ToolOutputCompactor.new, telemetry_logger: TelemetryLogger.new, context_budget_meter: nil, mcp_clients: nil)
64
66
  @workspace = workspace
65
67
  @prompt = prompt
66
68
  @web_search = web_search
@@ -75,6 +77,13 @@ module Kward
75
77
  @tool_output_compactor = tool_output_compactor
76
78
  @telemetry_logger = telemetry_logger
77
79
  @context_budget_meter = context_budget_meter
80
+ @mcp_clients = if mcp_clients
81
+ mcp_clients
82
+ elsif @allowed_tool_names
83
+ []
84
+ else
85
+ MCP::ServerConfig.clients_from_config(ConfigFiles.read_config)
86
+ end
78
87
  @tools = build_tools.freeze
79
88
  @schemas = build_schema_tools.map(&:schema).freeze
80
89
  end
@@ -182,6 +191,7 @@ module Kward
182
191
  "list_directory", "read_file", "write_file", "edit_file", "run_shell_command", "code_search", "summarize_file_structure", "context_for_task", "context_budget_stats", "retrieve_tool_output"
183
192
  )
184
193
  tools.concat(@tools.values_at("web_search", "fetch_content", "fetch_raw")) if web_search_available?
194
+ tools.concat(@tools.values.select { |tool| tool.is_a?(Tools::MCPTool) })
185
195
  tools << @tools["read_skill"] if skills_available?
186
196
  tools << @tools["ask_user_question"] if ask_user_question_available?
187
197
  tools.compact
@@ -194,7 +204,7 @@ module Kward
194
204
  Tools::FetchRaw.new(web_fetch: @web_fetch),
195
205
  Tools::ReadSkill.new,
196
206
  Tools::AskUserQuestion.new(prompt: @prompt)
197
- ]
207
+ ] + mcp_tool_values
198
208
  end
199
209
 
200
210
  def core_tools
@@ -224,6 +234,26 @@ module Kward
224
234
  skills.any?
225
235
  end
226
236
 
237
+ def mcp_tool_values
238
+ @mcp_tool_values ||= build_mcp_tools
239
+ end
240
+
241
+ def build_mcp_tools
242
+ Array(@mcp_clients).flat_map do |client|
243
+ client.list_tools.map do |tool|
244
+ Tools::MCPTool.new(server_name: client.name, client: client, tool: tool)
245
+ end
246
+ rescue StandardError => e
247
+ @telemetry_logger.log(
248
+ "mcp",
249
+ "server_unavailable",
250
+ "server" => client.respond_to?(:name) ? client.name : "unknown",
251
+ "error" => e.message
252
+ )
253
+ []
254
+ end
255
+ end
256
+
227
257
  def ask_user_question_available?
228
258
  return false if @ask_user_question_enabled == false
229
259
 
data/lib/kward/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # Namespace for the Kward CLI agent runtime.
2
2
  module Kward
3
3
  # Current gem version.
4
- VERSION = "0.73.1"
4
+ VERSION = "0.74.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kward
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.73.1
4
+ version: 0.74.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kai Wood
@@ -137,6 +137,7 @@ files:
137
137
  - doc/files.md
138
138
  - doc/getting-started.md
139
139
  - doc/git.md
140
+ - doc/mcp.md
140
141
  - doc/memory.md
141
142
  - doc/personas.md
142
143
  - doc/plugins.md
@@ -197,6 +198,9 @@ files:
197
198
  - lib/kward/local_command_runner.rb
198
199
  - lib/kward/local_pty_command_runner.rb
199
200
  - lib/kward/markdown_transcript.rb
201
+ - lib/kward/mcp/client.rb
202
+ - lib/kward/mcp/server_config.rb
203
+ - lib/kward/mcp/stdio_transport.rb
200
204
  - lib/kward/memory/manager.rb
201
205
  - lib/kward/message_access.rb
202
206
  - lib/kward/message_text.rb
@@ -305,6 +309,7 @@ files:
305
309
  - lib/kward/tools/fetch_content.rb
306
310
  - lib/kward/tools/fetch_raw.rb
307
311
  - lib/kward/tools/list_directory.rb
312
+ - lib/kward/tools/mcp_tool.rb
308
313
  - lib/kward/tools/read_file.rb
309
314
  - lib/kward/tools/read_skill.rb
310
315
  - lib/kward/tools/registry.rb