htm 0.0.10 → 0.0.11

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: b552d9b1e6a197c35d1ff2c64d4c8eb94a36a0e67d05efe1c10d305cf8f23d78
4
- data.tar.gz: 58fa199e1a3af9fb9c3a9e8fc0fe9ee779209a44baecbccdfdf6f570f5e7c692
3
+ metadata.gz: c6e41109b86fd5c51c8f96cec8a11ea9455035da54d72ae461b3b2f0a7ab7a8d
4
+ data.tar.gz: 1fe15917933aefa827a12c6e92406711ab95449097cb78bdab4c0f6510f595c1
5
5
  SHA512:
6
- metadata.gz: 9326f318f1677ea5cad5e5f6cdda4125421a13bde13d1c7bef0a2b2e8096873a7762f11d008c8d0e64365f90272aad58699b39a3af40f0c188f7e961eef6d2df
7
- data.tar.gz: 6f25fa0089349b0872db11427747e525ab46a9528d1ada22be183be260bad40d48ef692f78d922ba0803b817f2f661fa143721bf180882f34c575f4650e65448
6
+ metadata.gz: c02e326797a6986e1a5b987bec948f09c0ffc7cb45045898e3fc6faf5c57c9bf3ca4d399324e88528f994c7e3e1989bb71f462aae8adbb270b7c99654aa135d3
7
+ data.tar.gz: 4ee90ea84f36ab2d85c72cae4f5a1382e9a174d354f2d976d73b8915cb5dc8df68f6ae799ec082076293ce9b386500b94b4fe12d291903c0804351f5fd027151
@@ -84,7 +84,10 @@
84
84
  "WebFetch(domain:www.barndominiumlife.com)",
85
85
  "Bash(git rm:*)",
86
86
  "Bash(HTM_DBURL=\"postgresql://dewayne@localhost:5432/htm_development\" psql -c \"DROP TABLE IF EXISTS working_memories CASCADE;\" htm_development)",
87
- "Bash(git checkout:*)"
87
+ "Bash(git checkout:*)",
88
+ "WebFetch(domain:rubygems.org)",
89
+ "WebFetch(domain:www.rubyllm-mcp.com)",
90
+ "Bash(gem contents:*)"
88
91
  ],
89
92
  "deny": [],
90
93
  "ask": []
data/CHANGELOG.md CHANGED
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.0.11] - 2025-12-02
11
+
12
+ ### Added
13
+ - **MCP Server and Client** - Model Context Protocol integration for AI assistants
14
+ - `bin/htm_mcp.rb` - FastMCP-based server exposing HTM tools:
15
+ - `SetRobotTool` - Set robot identity for the session (per-client isolation)
16
+ - `GetRobotTool` - Get current robot information
17
+ - `GetWorkingMemoryTool` - Retrieve working memory for session restore
18
+ - `RememberTool` - Store information with tags and metadata
19
+ - `RecallTool` - Search memories with vector/fulltext/hybrid strategies
20
+ - `ForgetTool` / `RestoreTool` - Soft delete and restore memories
21
+ - `ListTagsTool` - List tags with optional prefix filtering
22
+ - `StatsTool` - Memory usage statistics
23
+ - Resources: `htm://statistics`, `htm://tags/hierarchy`, `htm://memories/recent`
24
+ - STDERR logging to avoid corrupting MCP JSON-RPC protocol
25
+ - Session-based robot identity via `MCPSession` module
26
+ - `examples/mcp_client.rb` - Interactive chat client using ruby_llm-mcp:
27
+ - Prompts for robot name on startup (or uses `HTM_ROBOT_NAME` env var)
28
+ - Session restore: offers to restore working memory from previous session
29
+ - Interactive chat loop with Ollama LLM (gpt-oss model)
30
+ - Tool call logging for visibility
31
+ - Slash commands: `/tools`, `/resources`, `/stats`, `/tags`, `/clear`, `/help`, `/exit`
32
+ - **Session restore feature** - MCP client can restore previous session context
33
+ - `GetWorkingMemoryTool` returns all nodes in working memory for a robot
34
+ - Client prompts user to restore previous session on startup
35
+ - Working memory injected into chat context for continuity
36
+
37
+ ### Fixed
38
+ - **GetWorkingMemoryTool** now uses `joins(:node)` to exclude soft-deleted nodes
39
+ - **StatsTool** fixed scope error (`Node.active` → `Node.count` with default_scope)
40
+ - **MCP tool response parsing** - Extract `.text` from `RubyLLM::MCP::Content` objects
41
+
10
42
  ## [0.0.10] - 2025-12-02
11
43
 
12
44
  ### Added
@@ -436,7 +468,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
436
468
  - Working memory size is user-configurable
437
469
  - See ADRs for detailed architectural decisions and rationale
438
470
 
439
- [Unreleased]: https://github.com/madbomber/htm/compare/v0.0.10...HEAD
471
+ [Unreleased]: https://github.com/madbomber/htm/compare/v0.0.12...HEAD
472
+ [0.0.12]: https://github.com/madbomber/htm/compare/v0.0.10...v0.0.12
440
473
  [0.0.10]: https://github.com/madbomber/htm/compare/v0.0.9...v0.0.10
441
474
  [0.0.9]: https://github.com/madbomber/htm/compare/v0.0.8...v0.0.9
442
475
  [0.0.8]: https://github.com/madbomber/htm/compare/v0.0.7...v0.0.8
data/README.md CHANGED
@@ -1400,13 +1400,13 @@ This separation allows you to provide any LLM implementation while HTM handles r
1400
1400
  ## Roadmap
1401
1401
 
1402
1402
  - [x] Phase 1: Foundation (basic two-tier memory)
1403
- - [ ] Phase 2: RAG retrieval (semantic search)
1404
- - [ ] Phase 3: Relationships & tags
1405
- - [ ] Phase 4: Working memory management
1406
- - [ ] Phase 5: Hive mind features
1407
- - [ ] Phase 6: Operations & observability
1408
- - [ ] Phase 7: Advanced features
1409
- - [ ] Phase 8: Production-ready gem
1403
+ - [x] Phase 2: RAG retrieval (semantic search)
1404
+ - [x] Phase 3: Relationships & tags
1405
+ - [x] Phase 4: Working memory management
1406
+ - [x] Phase 5: Hive mind features
1407
+ - [x] Phase 6: Operations & observability
1408
+ - [x] Phase 7: Advanced features
1409
+ - [x] Phase 8: Production-ready gem
1410
1410
 
1411
1411
 
1412
1412
  ## Contributing
data/bin/htm_mcp.rb ADDED
@@ -0,0 +1,527 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ # htm/bin/htm_mcp.rb
4
+
5
+ # MCP Server Example for HTM
6
+ #
7
+ # This example demonstrates using FastMCP to expose HTM's memory
8
+ # capabilities as an MCP (Model Context Protocol) server that can
9
+ # be used by AI assistants like Claude Desktop.
10
+ #
11
+ # Prerequisites:
12
+ # 1. Install fast-mcp gem: gem install fast-mcp
13
+ # 2. Set HTM_DBURL environment variable
14
+ # 3. Initialize database schema: rake db_setup
15
+ #
16
+ # Usage:
17
+ # ruby examples/mcp_server.rb
18
+ #
19
+ # The server uses STDIO transport by default, making it compatible
20
+ # with Claude Desktop and other MCP clients.
21
+ #
22
+ # Claude Desktop configuration (~/.config/claude/claude_desktop_config.json):
23
+ # {
24
+ # "mcpServers": {
25
+ # "htm-memory": {
26
+ # "command": "ruby",
27
+ # "args": ["/path/to/htm/examples/mcp_server.rb"],
28
+ # "env": {
29
+ # "HTM_DBURL": "postgresql://postgres@localhost:5432/htm_development"
30
+ # }
31
+ # }
32
+ # }
33
+ # }
34
+
35
+ require_relative '../lib/htm'
36
+
37
+ begin
38
+ require 'fast_mcp'
39
+ rescue LoadError
40
+ warn "Error: fast-mcp gem not found."
41
+ warn "Install it with: gem install fast-mcp"
42
+ exit 1
43
+ end
44
+
45
+ # Check environment
46
+ unless ENV['HTM_DBURL']
47
+ warn "Error: HTM_DBURL not set."
48
+ warn " export HTM_DBURL=\"postgresql://postgres@localhost:5432/htm_development\""
49
+ exit 1
50
+ end
51
+
52
+ # IMPORTANT: MCP uses STDIO for JSON-RPC communication.
53
+ # ALL logging must go to STDERR to avoid corrupting the protocol.
54
+ # Create a logger that writes to STDERR for MCP server diagnostics.
55
+ MCP_STDERR_LOG = Logger.new($stderr)
56
+ MCP_STDERR_LOG.level = Logger::INFO
57
+ MCP_STDERR_LOG.formatter = proc do |severity, datetime, _progname, msg|
58
+ "[MCP #{severity}] #{datetime.strftime('%H:%M:%S')} #{msg}\n"
59
+ end
60
+
61
+ # Silent logger for RubyLLM/HTM internals (prevents STDOUT corruption)
62
+ mcp_logger = Logger.new(IO::NULL)
63
+
64
+ # Configure RubyLLM to not log to STDOUT (corrupts MCP protocol)
65
+ require 'ruby_llm'
66
+ RubyLLM.configure do |config|
67
+ config.logger = mcp_logger
68
+ end
69
+
70
+ # Configure HTM
71
+ HTM.configure do |config|
72
+ config.job_backend = :inline # Synchronous for MCP responses
73
+ config.logger = mcp_logger # Silent logging for MCP
74
+ end
75
+
76
+ # Session state for the current robot
77
+ # Each MCP client spawns its own server process, so this is naturally isolated
78
+ module MCPSession
79
+ DEFAULT_ROBOT_NAME = "mcp_default"
80
+
81
+ class << self
82
+ def htm_instance
83
+ @htm_instance ||= HTM.new(robot_name: DEFAULT_ROBOT_NAME)
84
+ end
85
+
86
+ def set_robot(name)
87
+ @robot_name = name
88
+ @htm_instance = HTM.new(robot_name: name)
89
+ MCP_STDERR_LOG.info "Robot set: #{name} (id=#{@htm_instance.robot_id})"
90
+ @htm_instance
91
+ end
92
+
93
+ def robot_name
94
+ @robot_name || DEFAULT_ROBOT_NAME
95
+ end
96
+
97
+ def robot_initialized?
98
+ @robot_name != nil
99
+ end
100
+ end
101
+ end
102
+
103
+ # Tool: Set the robot identity for this session
104
+ class SetRobotTool < FastMcp::Tool
105
+ description "Set the robot identity for this session. Call this first to establish your robot name."
106
+
107
+ arguments do
108
+ required(:name).filled(:string).description("The robot name (will be created if it doesn't exist)")
109
+ end
110
+
111
+ def call(name:)
112
+ MCP_STDERR_LOG.info "SetRobotTool called: name=#{name.inspect}"
113
+
114
+ htm = MCPSession.set_robot(name)
115
+ robot = HTM::Models::Robot.find(htm.robot_id)
116
+
117
+ {
118
+ success: true,
119
+ robot_id: htm.robot_id,
120
+ robot_name: htm.robot_name,
121
+ node_count: robot.node_count,
122
+ message: "Robot '#{name}' is now active for this session"
123
+ }.to_json
124
+ end
125
+ end
126
+
127
+ # Tool: Get current robot info
128
+ class GetRobotTool < FastMcp::Tool
129
+ description "Get information about the current robot for this session"
130
+
131
+ arguments do
132
+ end
133
+
134
+ def call
135
+ MCP_STDERR_LOG.info "GetRobotTool called"
136
+
137
+ htm = MCPSession.htm_instance
138
+ robot = HTM::Models::Robot.find(htm.robot_id)
139
+
140
+ {
141
+ success: true,
142
+ robot_id: htm.robot_id,
143
+ robot_name: htm.robot_name,
144
+ initialized: MCPSession.robot_initialized?,
145
+ memory_summary: robot.memory_summary
146
+ }.to_json
147
+ end
148
+ end
149
+
150
+ # Tool: Get working memory contents for session restore
151
+ class GetWorkingMemoryTool < FastMcp::Tool
152
+ description "Get all working memory contents for the current robot. Use this to restore a previous session."
153
+
154
+ arguments do
155
+ end
156
+
157
+ def call
158
+ htm = MCPSession.htm_instance
159
+ robot = HTM::Models::Robot.find(htm.robot_id)
160
+ MCP_STDERR_LOG.info "GetWorkingMemoryTool called for robot=#{htm.robot_name}"
161
+
162
+ # Get all nodes in working memory with their metadata
163
+ # Filter out any robot_nodes where the node has been deleted (node uses default_scope)
164
+ working_memory_nodes = robot.robot_nodes
165
+ .in_working_memory
166
+ .joins(:node) # Inner join excludes deleted nodes
167
+ .includes(node: :tags)
168
+ .order(last_remembered_at: :desc)
169
+ .filter_map do |rn|
170
+ next unless rn.node # Extra safety check
171
+
172
+ {
173
+ id: rn.node.id,
174
+ content: rn.node.content,
175
+ tags: rn.node.tags.map(&:name),
176
+ remember_count: rn.remember_count,
177
+ last_remembered_at: rn.last_remembered_at&.iso8601,
178
+ created_at: rn.node.created_at.iso8601
179
+ }
180
+ end
181
+
182
+ MCP_STDERR_LOG.info "GetWorkingMemoryTool complete: #{working_memory_nodes.length} nodes in working memory"
183
+
184
+ {
185
+ success: true,
186
+ robot_id: htm.robot_id,
187
+ robot_name: htm.robot_name,
188
+ count: working_memory_nodes.length,
189
+ working_memory: working_memory_nodes
190
+ }.to_json
191
+ rescue StandardError => e
192
+ MCP_STDERR_LOG.error "GetWorkingMemoryTool error: #{e.message}"
193
+ { success: false, error: e.message, count: 0, working_memory: [] }.to_json
194
+ end
195
+ end
196
+
197
+ # Tool: Remember information
198
+ class RememberTool < FastMcp::Tool
199
+ description "Store information in HTM long-term memory with optional tags"
200
+
201
+ arguments do
202
+ required(:content).filled(:string).description("The content to remember")
203
+ optional(:tags).array(:string).description("Optional tags for categorization (e.g., ['database:postgresql', 'config'])")
204
+ optional(:metadata).hash.description("Optional metadata key-value pairs")
205
+ end
206
+
207
+ def call(content:, tags: [], metadata: {})
208
+ MCP_STDERR_LOG.info "RememberTool called: content=#{content[0..50].inspect}..."
209
+
210
+ htm = MCPSession.htm_instance
211
+ node_id = htm.remember(content, tags: tags, metadata: metadata)
212
+ node = HTM::Models::Node.includes(:tags).find(node_id)
213
+
214
+ MCP_STDERR_LOG.info "Memory stored: node_id=#{node_id}, robot=#{htm.robot_name}, tags=#{node.tags.map(&:name)}"
215
+
216
+ {
217
+ success: true,
218
+ node_id: node_id,
219
+ robot_id: htm.robot_id,
220
+ robot_name: htm.robot_name,
221
+ content: node.content,
222
+ tags: node.tags.map(&:name),
223
+ created_at: node.created_at.iso8601
224
+ }.to_json
225
+ end
226
+ end
227
+
228
+ # Tool: Recall memories
229
+ class RecallTool < FastMcp::Tool
230
+ description "Search and retrieve memories from HTM using semantic, full-text, or hybrid search"
231
+
232
+ arguments do
233
+ required(:query).filled(:string).description("Search query - can be natural language or keywords")
234
+ optional(:limit).filled(:integer).description("Maximum number of results (default: 10)")
235
+ optional(:strategy).filled(:string).description("Search strategy: 'vector', 'fulltext', or 'hybrid' (default: 'hybrid')")
236
+ optional(:timeframe).filled(:string).description("Filter by time: 'today', 'this week', 'this month', or ISO8601 date range")
237
+ end
238
+
239
+ def call(query:, limit: 10, strategy: 'hybrid', timeframe: nil)
240
+ htm = MCPSession.htm_instance
241
+ MCP_STDERR_LOG.info "RecallTool called: query=#{query.inspect}, strategy=#{strategy}, limit=#{limit}, robot=#{htm.robot_name}"
242
+
243
+ recall_opts = {
244
+ limit: limit,
245
+ strategy: strategy.to_sym,
246
+ raw: true
247
+ }
248
+
249
+ # Parse timeframe if provided
250
+ if timeframe
251
+ recall_opts[:timeframe] = parse_timeframe(timeframe)
252
+ end
253
+
254
+ memories = htm.recall(query, **recall_opts)
255
+
256
+ results = memories.map do |memory|
257
+ node = HTM::Models::Node.includes(:tags).find(memory['id'])
258
+ {
259
+ id: memory['id'],
260
+ content: memory['content'],
261
+ tags: node.tags.map(&:name),
262
+ created_at: memory['created_at'],
263
+ score: memory['combined_score'] || memory['similarity']
264
+ }
265
+ end
266
+
267
+ MCP_STDERR_LOG.info "Recall complete: found #{results.length} memories"
268
+
269
+ {
270
+ success: true,
271
+ query: query,
272
+ strategy: strategy,
273
+ robot_name: htm.robot_name,
274
+ count: results.length,
275
+ results: results
276
+ }.to_json
277
+ end
278
+
279
+ private
280
+
281
+ def parse_timeframe(timeframe)
282
+ case timeframe.downcase
283
+ when 'today'
284
+ Time.now.beginning_of_day..Time.now
285
+ when 'this week'
286
+ 1.week.ago..Time.now
287
+ when 'this month'
288
+ 1.month.ago..Time.now
289
+ else
290
+ # Try to parse as ISO8601 range (start..end)
291
+ if timeframe.include?('..')
292
+ parts = timeframe.split('..')
293
+ Time.parse(parts[0])..Time.parse(parts[1])
294
+ else
295
+ # Single date - from that date to now
296
+ Time.parse(timeframe)..Time.now
297
+ end
298
+ end
299
+ rescue ArgumentError
300
+ nil # Invalid timeframe, skip filtering
301
+ end
302
+ end
303
+
304
+ # Tool: Forget a memory
305
+ class ForgetTool < FastMcp::Tool
306
+ description "Soft-delete a memory from HTM (can be restored later)"
307
+
308
+ arguments do
309
+ required(:node_id).filled(:integer).description("The ID of the node to forget")
310
+ end
311
+
312
+ def call(node_id:)
313
+ htm = MCPSession.htm_instance
314
+ MCP_STDERR_LOG.info "ForgetTool called: node_id=#{node_id}, robot=#{htm.robot_name}"
315
+
316
+ htm.forget(node_id)
317
+
318
+ MCP_STDERR_LOG.info "Memory soft-deleted: node_id=#{node_id}"
319
+
320
+ {
321
+ success: true,
322
+ node_id: node_id,
323
+ robot_name: htm.robot_name,
324
+ message: "Memory soft-deleted. Use restore to recover."
325
+ }.to_json
326
+ rescue ActiveRecord::RecordNotFound
327
+ MCP_STDERR_LOG.warn "ForgetTool failed: node #{node_id} not found"
328
+ {
329
+ success: false,
330
+ error: "Node #{node_id} not found"
331
+ }.to_json
332
+ end
333
+ end
334
+
335
+ # Tool: Restore a forgotten memory
336
+ class RestoreTool < FastMcp::Tool
337
+ description "Restore a soft-deleted memory"
338
+
339
+ arguments do
340
+ required(:node_id).filled(:integer).description("The ID of the node to restore")
341
+ end
342
+
343
+ def call(node_id:)
344
+ htm = MCPSession.htm_instance
345
+ MCP_STDERR_LOG.info "RestoreTool called: node_id=#{node_id}, robot=#{htm.robot_name}"
346
+
347
+ htm.restore(node_id)
348
+
349
+ MCP_STDERR_LOG.info "Memory restored: node_id=#{node_id}"
350
+
351
+ {
352
+ success: true,
353
+ node_id: node_id,
354
+ robot_name: htm.robot_name,
355
+ message: "Memory restored successfully"
356
+ }.to_json
357
+ rescue ActiveRecord::RecordNotFound
358
+ MCP_STDERR_LOG.warn "RestoreTool failed: node #{node_id} not found"
359
+ {
360
+ success: false,
361
+ error: "Node #{node_id} not found"
362
+ }.to_json
363
+ end
364
+ end
365
+
366
+ # Tool: List tags
367
+ class ListTagsTool < FastMcp::Tool
368
+ description "List all tags in HTM, optionally filtered by prefix"
369
+
370
+ arguments do
371
+ optional(:prefix).filled(:string).description("Filter tags by prefix (e.g., 'database' returns 'database:postgresql', etc.)")
372
+ end
373
+
374
+ def call(prefix: nil)
375
+ MCP_STDERR_LOG.info "ListTagsTool called: prefix=#{prefix.inspect}"
376
+
377
+ tags_query = HTM::Models::Tag.order(:name)
378
+ tags_query = tags_query.where("name LIKE ?", "#{prefix}%") if prefix
379
+
380
+ tags = tags_query.map do |tag|
381
+ {
382
+ name: tag.name,
383
+ node_count: tag.nodes.count
384
+ }
385
+ end
386
+
387
+ MCP_STDERR_LOG.info "ListTagsTool complete: found #{tags.length} tags"
388
+
389
+ {
390
+ success: true,
391
+ prefix: prefix,
392
+ count: tags.length,
393
+ tags: tags
394
+ }.to_json
395
+ end
396
+ end
397
+
398
+ # Tool: Get memory statistics
399
+ class StatsTool < FastMcp::Tool
400
+ description "Get statistics about HTM memory usage"
401
+
402
+ arguments do
403
+ end
404
+
405
+ def call
406
+ htm = MCPSession.htm_instance
407
+ robot = HTM::Models::Robot.find(htm.robot_id)
408
+ MCP_STDERR_LOG.info "StatsTool called for robot=#{htm.robot_name}"
409
+
410
+ # Note: Node uses default_scope to exclude deleted, so .count returns active nodes
411
+ total_nodes = HTM::Models::Node.count
412
+ deleted_nodes = HTM::Models::Node.deleted.count
413
+ nodes_with_embeddings = HTM::Models::Node.with_embeddings.count
414
+ nodes_with_tags = HTM::Models::Node.joins(:tags).distinct.count
415
+ total_tags = HTM::Models::Tag.count
416
+ total_robots = HTM::Models::Robot.count
417
+
418
+ MCP_STDERR_LOG.info "StatsTool complete: #{total_nodes} active nodes, #{total_tags} tags"
419
+
420
+ {
421
+ success: true,
422
+ current_robot: {
423
+ name: htm.robot_name,
424
+ id: htm.robot_id,
425
+ memory_summary: robot.memory_summary
426
+ },
427
+ statistics: {
428
+ nodes: {
429
+ active: total_nodes,
430
+ deleted: deleted_nodes,
431
+ with_embeddings: nodes_with_embeddings,
432
+ with_tags: nodes_with_tags
433
+ },
434
+ tags: {
435
+ total: total_tags
436
+ },
437
+ robots: {
438
+ total: total_robots
439
+ }
440
+ }
441
+ }.to_json
442
+ rescue StandardError => e
443
+ MCP_STDERR_LOG.error "StatsTool error: #{e.message}"
444
+ { success: false, error: e.message }.to_json
445
+ end
446
+ end
447
+
448
+ # Resource: Memory Statistics
449
+ class MemoryStatsResource < FastMcp::Resource
450
+ uri "htm://statistics"
451
+ resource_name "HTM Memory Statistics"
452
+ mime_type "application/json"
453
+
454
+ def content
455
+ htm = MCPSession.htm_instance
456
+ {
457
+ total_nodes: HTM::Models::Node.active.count,
458
+ total_tags: HTM::Models::Tag.count,
459
+ total_robots: HTM::Models::Robot.count,
460
+ current_robot: htm.robot_name,
461
+ robot_id: htm.robot_id,
462
+ robot_initialized: MCPSession.robot_initialized?,
463
+ embedding_provider: HTM.configuration.embedding_provider,
464
+ embedding_model: HTM.configuration.embedding_model
465
+ }.to_json
466
+ end
467
+ end
468
+
469
+ # Resource: Tag Hierarchy
470
+ class TagHierarchyResource < FastMcp::Resource
471
+ uri "htm://tags/hierarchy"
472
+ resource_name "HTM Tag Hierarchy"
473
+ mime_type "text/plain"
474
+
475
+ def content
476
+ HTM::Models::Tag.all.tree_string
477
+ end
478
+ end
479
+
480
+ # Resource: Recent Memories
481
+ class RecentMemoriesResource < FastMcp::Resource
482
+ uri "htm://memories/recent"
483
+ resource_name "Recent HTM Memories"
484
+ mime_type "application/json"
485
+
486
+ def content
487
+ recent = HTM::Models::Node.active
488
+ .includes(:tags)
489
+ .order(created_at: :desc)
490
+ .limit(20)
491
+ .map do |node|
492
+ {
493
+ id: node.id,
494
+ content: node.content[0..200],
495
+ tags: node.tags.map(&:name),
496
+ created_at: node.created_at.iso8601
497
+ }
498
+ end
499
+
500
+ { recent_memories: recent }.to_json
501
+ end
502
+ end
503
+
504
+ # Create and configure the MCP server
505
+ server = FastMcp::Server.new(
506
+ name: 'htm-memory-server',
507
+ version: HTM::VERSION
508
+ )
509
+
510
+ # Register tools
511
+ server.register_tool(SetRobotTool) # Call first to set robot identity
512
+ server.register_tool(GetRobotTool) # Get current robot info
513
+ server.register_tool(GetWorkingMemoryTool) # Get working memory for session restore
514
+ server.register_tool(RememberTool)
515
+ server.register_tool(RecallTool)
516
+ server.register_tool(ForgetTool)
517
+ server.register_tool(RestoreTool)
518
+ server.register_tool(ListTagsTool)
519
+ server.register_tool(StatsTool)
520
+
521
+ # Register resources
522
+ server.register_resource(MemoryStatsResource)
523
+ server.register_resource(TagHierarchyResource)
524
+ server.register_resource(RecentMemoriesResource)
525
+
526
+ # Start the server (STDIO transport by default)
527
+ server.start
data/examples/README.md CHANGED
@@ -107,6 +107,127 @@ ruby examples/timeframe_demo.rb
107
107
 
108
108
  ---
109
109
 
110
+ ### mcp_server.rb & mcp_client.rb
111
+
112
+ **Model Context Protocol (MCP) integration for AI assistants.**
113
+
114
+ A pair of examples demonstrating how to expose HTM as an MCP server and connect to it from a chat client. This enables AI assistants like Claude Desktop to use HTM's memory capabilities.
115
+
116
+ #### mcp_server.rb
117
+
118
+ An MCP server that exposes HTM's memory operations as tools:
119
+
120
+ ```bash
121
+ ruby examples/mcp_server.rb
122
+ ```
123
+
124
+ **Tools exposed:**
125
+ - `SetRobotTool` - Set the robot identity for this session (call first)
126
+ - `GetRobotTool` - Get current robot information
127
+ - `GetWorkingMemoryTool` - Get working memory contents for session restore
128
+ - `RememberTool` - Store information with optional tags and metadata
129
+ - `RecallTool` - Search memories using vector, fulltext, or hybrid strategies
130
+ - `ForgetTool` - Soft-delete a memory (recoverable)
131
+ - `RestoreTool` - Restore a soft-deleted memory
132
+ - `ListTagsTool` - List tags with optional prefix filtering
133
+ - `StatsTool` - Get memory usage statistics
134
+
135
+ **Resources exposed:**
136
+ - `htm://statistics` - Memory statistics as JSON
137
+ - `htm://tags/hierarchy` - Tag hierarchy as text tree
138
+ - `htm://memories/recent` - Last 20 memories
139
+
140
+ **Claude Desktop configuration** (`~/.config/claude/claude_desktop_config.json`):
141
+ ```json
142
+ {
143
+ "mcpServers": {
144
+ "htm-memory": {
145
+ "command": "ruby",
146
+ "args": ["/path/to/htm/examples/mcp_server.rb"],
147
+ "env": {
148
+ "HTM_DBURL": "postgresql://user@localhost:5432/htm_development"
149
+ }
150
+ }
151
+ }
152
+ }
153
+ ```
154
+
155
+ #### mcp_client.rb
156
+
157
+ An interactive chat client that connects to the MCP server via STDIO and uses a local Ollama model (gpt-oss) for conversation:
158
+
159
+ ```bash
160
+ ruby examples/mcp_client.rb
161
+ ```
162
+
163
+ **Features:**
164
+ - Prompts for robot name on startup (or uses `HTM_ROBOT_NAME` env var)
165
+ - Calls `SetRobotTool` to establish robot identity with the server
166
+ - Offers to restore previous session from working memory
167
+ - Connects to `mcp_server.rb` automatically via STDIO transport
168
+ - Interactive chat loop with tool calling
169
+ - LLM decides when to remember/recall information
170
+ - Logs tool calls and results for visibility
171
+
172
+ **Commands:**
173
+ - `/tools` - List available MCP tools
174
+ - `/resources` - List available MCP resources
175
+ - `/clear` - Clear chat history
176
+ - `/help` - Show help
177
+ - `/exit` - Quit
178
+
179
+ **Example startup and conversation:**
180
+ ```
181
+ $ ruby examples/mcp_client.rb
182
+ Connecting to HTM MCP server...
183
+ [✓] Connected to HTM MCP server
184
+ [✓] Found 9 tools:
185
+ - SetRobotTool: Set the robot identity for this session...
186
+ - GetRobotTool: Get information about the current robot...
187
+ - GetWorkingMemoryTool: Get all working memory contents...
188
+ ...
189
+
190
+ Enter your robot name (or press Enter for default): alice-assistant
191
+ [✓] Robot name: alice-assistant
192
+ Setting robot identity on MCP server...
193
+ [✓] Robot identity set: alice-assistant (id=5, nodes=12)
194
+
195
+ Found 3 memories in working memory from previous session.
196
+ Restore previous session? (y/N): y
197
+ [✓] Will restore 3 memories after chat setup
198
+
199
+ Initializing chat with gpt-oss:latest...
200
+ [✓] Chat initialized with tools attached
201
+ Restoring 3 memories to chat context...
202
+ [✓] Restored 3 memories to chat context
203
+
204
+ ======================================================================
205
+ HTM MCP Client - AI Chat with Memory Tools
206
+ ======================================================================
207
+
208
+ Robot: alice-assistant
209
+ Model: gpt-oss:latest (via Ollama)
210
+ ...
211
+
212
+ you> What's the API rate limit?
213
+
214
+ assistant> The API rate limit is 1000 requests per minute.
215
+ ```
216
+
217
+ **Additional dependencies:**
218
+ ```bash
219
+ gem install fast-mcp ruby_llm-mcp
220
+ ollama pull gpt-oss # Or your preferred model
221
+ ```
222
+
223
+ **Environment Variables:**
224
+ - `HTM_DBURL` - PostgreSQL connection (required)
225
+ - `OLLAMA_URL` - Ollama server URL (default: http://localhost:11434)
226
+ - `OLLAMA_MODEL` - Model to use (default: gpt-oss:latest)
227
+ - `HTM_ROBOT_NAME` - Robot name (optional, prompts if not set)
228
+
229
+ ---
230
+
110
231
  ## Application Examples
111
232
 
112
233
  ### example_app/
@@ -245,6 +366,8 @@ examples/
245
366
  ├── custom_llm_configuration.rb # LLM integration patterns
246
367
  ├── file_loader_usage.rb # Document loading
247
368
  ├── timeframe_demo.rb # Time-based filtering
369
+ ├── mcp_server.rb # MCP server exposing HTM tools
370
+ ├── mcp_client.rb # MCP client with chat interface
248
371
  ├── example_app/
249
372
  │ ├── app.rb # Full-featured demo app
250
373
  │ └── Rakefile
@@ -274,6 +397,8 @@ examples/
274
397
  | Custom LLM integration | `custom_llm_configuration.rb` |
275
398
  | Loading documents/files | `file_loader_usage.rb` |
276
399
  | Time-based queries | `timeframe_demo.rb` |
400
+ | MCP server for AI assistants | `mcp_server.rb` |
401
+ | MCP client with chat interface | `mcp_client.rb` |
277
402
  | Web application | `sinatra_app/` |
278
403
  | CLI tool | `cli_app/` |
279
404
  | Multi-robot coordination | `robot_groups/` |
@@ -0,0 +1,529 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # MCP Client Example for HTM
5
+ #
6
+ # This example demonstrates using ruby_llm-mcp to connect to the
7
+ # HTM MCP server and interact with it through a chat interface
8
+ # using a local Ollama model.
9
+ #
10
+ # Prerequisites:
11
+ # 1. Install gems: gem install ruby_llm-mcp
12
+ # 2. Have Ollama running with gpt-oss model: ollama pull gpt-oss
13
+ # 3. Set HTM_DBURL environment variable
14
+ # 4. The htm_mcp.rb must be available (this client will launch it)
15
+ #
16
+ # Usage:
17
+ # ruby examples/mcp_client.rb
18
+ #
19
+ # The client connects to the HTM MCP server via STDIO transport
20
+ # and provides an interactive chat loop where you can:
21
+ # - Ask the LLM to remember information
22
+ # - Query memories using natural language
23
+ # - List tags and statistics
24
+ # - All through conversational AI with tool calling
25
+
26
+ require 'ruby_llm'
27
+ require 'ruby_llm/mcp'
28
+
29
+ # Configuration
30
+ OLLAMA_MODEL = ENV.fetch('OLLAMA_MODEL', 'gpt-oss:latest')
31
+ OLLAMA_URL = ENV.fetch('OLLAMA_URL', 'http://localhost:11434')
32
+ MCP_SERVER_PATH = File.expand_path('../bin/htm_mcp.rb', __dir__)
33
+ ENV_ROBOT_NAME = ENV['HTM_ROBOT_NAME'] # nil if not set, allows prompting
34
+
35
+ class HTMMcpClient
36
+ attr_reader :robot_name
37
+
38
+ def initialize
39
+ validate_environment
40
+ setup_ruby_llm
41
+ setup_mcp_client
42
+ prompt_for_robot_name
43
+ set_robot_identity
44
+ check_working_memory
45
+ setup_chat
46
+ restore_session_if_requested
47
+ end
48
+
49
+
50
+ def run
51
+ print_banner
52
+ chat_loop
53
+ ensure
54
+ cleanup
55
+ end
56
+
57
+ private
58
+
59
+ def validate_environment
60
+ unless ENV['HTM_DBURL']
61
+ warn 'Error: HTM_DBURL not set.'
62
+ warn ' export HTM_DBURL="postgresql://postgres@localhost:5432/htm_development"'
63
+ exit 1
64
+ end
65
+
66
+ unless File.exist?(MCP_SERVER_PATH)
67
+ warn "Error: MCP server not found at #{MCP_SERVER_PATH}"
68
+ warn 'Please ensure mcp_server.rb exists in the examples directory.'
69
+ exit 1
70
+ end
71
+
72
+ # Check Ollama is running
73
+ require 'net/http'
74
+ begin
75
+ uri = URI(OLLAMA_URL)
76
+ Net::HTTP.get_response(uri)
77
+ rescue StandardError => e
78
+ warn "Error: Cannot connect to Ollama at #{OLLAMA_URL}"
79
+ warn " #{e.message}"
80
+ warn 'Please ensure Ollama is running: ollama serve'
81
+ exit 1
82
+ end
83
+ end
84
+
85
+
86
+ def setup_ruby_llm
87
+ # Configure RubyLLM for Ollama
88
+ RubyLLM.configure do |config|
89
+ ollama_api_base = OLLAMA_URL.end_with?('/v1') ? OLLAMA_URL : "#{OLLAMA_URL}/v1"
90
+ config.ollama_api_base = ollama_api_base
91
+ end
92
+ end
93
+
94
+
95
+ def setup_mcp_client
96
+ puts 'Connecting to HTM MCP server...'
97
+
98
+ @mcp_client = RubyLLM::MCP.client(
99
+ name: 'htm-memory',
100
+ transport_type: :stdio,
101
+ request_timeout: 60_000, # 60 seconds (in ms) for Ollama embedding generation
102
+ config: {
103
+ command: RbConfig.ruby,
104
+ args: [MCP_SERVER_PATH],
105
+ env: {
106
+ 'HTM_DBURL' => ENV['HTM_DBURL'],
107
+ 'OLLAMA_URL' => OLLAMA_URL
108
+ }
109
+ }
110
+ )
111
+
112
+ # Wait for server to be ready
113
+ sleep 0.5
114
+
115
+ if @mcp_client.alive?
116
+ puts '[✓] Connected to HTM MCP server'
117
+ else
118
+ warn '[✗] Failed to connect to HTM MCP server'
119
+ exit 1
120
+ end
121
+
122
+ # List available tools
123
+ @tools = @mcp_client.tools
124
+ puts "[✓] Found #{@tools.length} tools:"
125
+ @tools.each do |tool|
126
+ puts " - #{tool.name}: #{tool.description[0..60]}..."
127
+ end
128
+ puts
129
+ end
130
+
131
+
132
+ def prompt_for_robot_name
133
+ if ENV_ROBOT_NAME
134
+ @robot_name = ENV_ROBOT_NAME
135
+ puts "[✓] Using robot name from HTM_ROBOT_NAME: #{@robot_name}"
136
+ return
137
+ end
138
+
139
+ puts
140
+ print 'Enter your robot name (or press Enter for "Assistant"): '
141
+ input = gets&.chomp
142
+
143
+ @robot_name = if input.nil? || input.empty?
144
+ 'Assistant'
145
+ else
146
+ input
147
+ end
148
+
149
+ puts "[✓] Robot name: #{@robot_name}"
150
+ end
151
+
152
+
153
+ def set_robot_identity
154
+ puts 'Setting robot identity on MCP server...'
155
+
156
+ # Find the SetRobotTool
157
+ set_robot_tool = @tools.find { |t| t.name == 'SetRobotTool' }
158
+
159
+ unless set_robot_tool
160
+ warn '[⚠] SetRobotTool not found - using default robot identity'
161
+ return
162
+ end
163
+
164
+ # Call SetRobotTool directly via MCP
165
+ result = set_robot_tool.call(name: @robot_name)
166
+
167
+ # Parse the result
168
+ begin
169
+ response = JSON.parse(result.is_a?(String) ? result : result.to_s)
170
+ if response['success']
171
+ puts "[✓] Robot identity set: #{response['robot_name']} (id=#{response['robot_id']}, nodes=#{response['node_count']})"
172
+ else
173
+ warn "[⚠] Failed to set robot identity: #{response['error']}"
174
+ end
175
+ rescue JSON::ParserError => e
176
+ warn "[⚠] Could not parse SetRobotTool response: #{e.message}"
177
+ end
178
+ puts
179
+ end
180
+
181
+
182
+ def check_working_memory
183
+ # Check if there's working memory to restore
184
+ get_wm_tool = @tools.find { |t| t.name == 'GetWorkingMemoryTool' }
185
+ @working_memory_to_restore = nil
186
+ return unless get_wm_tool
187
+
188
+ result = get_wm_tool.call({})
189
+
190
+ # Extract JSON from MCP Content object
191
+ json_str = result.respond_to?(:text) ? result.text : result.to_s
192
+ response = JSON.parse(json_str)
193
+
194
+ # Debug: show what we got
195
+ puts "[Debug] Working memory check: success=#{response['success']}, count=#{response['count']}"
196
+
197
+ return unless response['success'] && response['count'].to_i > 0
198
+
199
+ puts "Found #{response['count']} memories in working memory from previous session."
200
+ print 'Restore previous session? (y/N): '
201
+ input = gets&.chomp&.downcase
202
+
203
+ if %w[y yes].include?(input)
204
+ @working_memory_to_restore = response['working_memory']
205
+ puts "[✓] Will restore #{@working_memory_to_restore.length} memories after chat setup"
206
+ else
207
+ puts '[✓] Starting fresh session'
208
+ end
209
+ puts
210
+ rescue JSON::ParserError => e
211
+ warn "[⚠] Could not parse working memory response: #{e.message}"
212
+ warn "[⚠] Raw: #{json_str.inspect[0..200]}" if defined?(json_str)
213
+ rescue StandardError => e
214
+ warn "[⚠] Error checking working memory: #{e.class} - #{e.message}"
215
+ end
216
+
217
+
218
+ def restore_session_if_requested
219
+ return unless @working_memory_to_restore
220
+
221
+ puts "Restoring #{@working_memory_to_restore.length} memories to chat context..."
222
+
223
+ # Build a system context from the working memory
224
+ context_parts = @working_memory_to_restore.map do |mem|
225
+ tags_str = mem['tags'].empty? ? '' : " [#{mem['tags'].join(', ')}]"
226
+ "- #{mem['content']}#{tags_str}"
227
+ end
228
+
229
+ context_message = <<~CONTEXT
230
+ The following information was remembered from your previous session with this user:
231
+
232
+ #{context_parts.join("\n")}
233
+
234
+ Use this context to inform your responses, but don't explicitly mention that you're restoring a session unless asked.
235
+ CONTEXT
236
+
237
+ # Add the context as a system message to prime the chat
238
+ @chat.add_message(role: :user, content: "Previous session context: #{context_message}")
239
+ @chat.add_message(role: :assistant,
240
+ content: "I've restored context from our previous session. How can I help you today?")
241
+
242
+ puts "[✓] Restored #{@working_memory_to_restore.length} memories to chat context"
243
+ puts
244
+ end
245
+
246
+
247
+ def setup_chat
248
+ puts "Initializing chat with #{OLLAMA_MODEL}..."
249
+
250
+ @chat = RubyLLM.chat(
251
+ model: OLLAMA_MODEL,
252
+ provider: :ollama,
253
+ assume_model_exists: true
254
+ )
255
+
256
+ # Attach MCP tools to the chat
257
+ @chat.with_tools(*@tools)
258
+
259
+ # Set up tool call logging
260
+ setup_tool_callbacks
261
+
262
+ puts '[✓] Chat initialized with tools attached'
263
+ end
264
+
265
+
266
+ def print_banner
267
+ puts
268
+ puts '=' * 70
269
+ puts 'HTM MCP Client - AI Chat with Memory Tools'
270
+ puts '=' * 70
271
+ puts
272
+ puts "Robot: #{@robot_name}"
273
+ puts "Model: #{OLLAMA_MODEL} (via Ollama)"
274
+ puts "MCP Server: #{MCP_SERVER_PATH}"
275
+ puts
276
+ puts 'Available tools:'
277
+ @tools.each do |tool|
278
+ puts " • #{tool.name}"
279
+ end
280
+ puts
281
+ puts 'Example queries:'
282
+ puts ' "Remember that the API rate limit is 1000 requests per minute"'
283
+ puts ' "What do you know about databases?"'
284
+ puts ' "Show me the memory statistics"'
285
+ puts ' "List all tags"'
286
+ puts ' "Forget node 123"'
287
+ puts
288
+ puts 'Commands:'
289
+ puts ' /tools - List available MCP tools'
290
+ puts ' /resources - List available MCP resources'
291
+ puts ' /stats - Show memory statistics'
292
+ puts ' /tags - List all tags'
293
+ puts ' /clear - Clear chat history'
294
+ puts ' /help - Show this help'
295
+ puts ' /exit - Quit'
296
+ puts
297
+ puts '=' * 70
298
+ puts
299
+ end
300
+
301
+
302
+ def chat_loop
303
+ loop do
304
+ print 'you> '
305
+ input = gets&.chomp
306
+ break if input.nil?
307
+
308
+ next if input.empty?
309
+
310
+ # Handle commands
311
+ case input
312
+ when '/exit', '/quit', '/q'
313
+ break
314
+ when '/tools'
315
+ show_tools
316
+ next
317
+ when '/resources'
318
+ show_resources
319
+ next
320
+ when '/stats'
321
+ show_stats
322
+ next
323
+ when '/tags'
324
+ show_tags
325
+ next
326
+ when '/clear'
327
+ clear_chat
328
+ next
329
+ when '/help'
330
+ print_banner
331
+ next
332
+ end
333
+
334
+ # Send to LLM with tools
335
+ begin
336
+ print "\n#{@robot_name}> "
337
+ response = @chat.ask(input)
338
+ puts response.content
339
+ puts
340
+ rescue StandardError => e
341
+ puts "\n[✗] Error: #{e.message}"
342
+ puts " #{e.class}: #{e.backtrace.first}"
343
+ puts
344
+ end
345
+ end
346
+
347
+ puts "\nGoodbye!"
348
+ end
349
+
350
+
351
+ def show_tools
352
+ puts "\nAvailable MCP Tools:"
353
+ puts '-' * 50
354
+ @tools.each do |tool|
355
+ puts
356
+ puts "#{tool.name}"
357
+ puts " #{tool.description}"
358
+ end
359
+ puts
360
+ end
361
+
362
+
363
+ def show_resources
364
+ puts "\nAvailable MCP Resources:"
365
+ puts '-' * 50
366
+ begin
367
+ resources = @mcp_client.resources
368
+ if resources.empty?
369
+ puts ' (no resources available)'
370
+ else
371
+ resources.each do |resource|
372
+ puts
373
+ puts "#{resource.uri}"
374
+ puts " #{resource.name}" if resource.respond_to?(:name)
375
+ end
376
+ end
377
+ rescue StandardError => e
378
+ puts " Error fetching resources: #{e.message}"
379
+ end
380
+ puts
381
+ end
382
+
383
+
384
+ def show_stats
385
+ puts "\nMemory Statistics:"
386
+ puts '-' * 50
387
+
388
+ stats_tool = @tools.find { |t| t.name == 'StatsTool' }
389
+ unless stats_tool
390
+ puts ' StatsTool not available'
391
+ puts
392
+ return
393
+ end
394
+
395
+ result = stats_tool.call({})
396
+
397
+ # Handle different response types from MCP
398
+ # RubyLLM::MCP::Content objects have a .text attribute
399
+ json_str = case result
400
+ when String then result
401
+ when Hash then result.to_json
402
+ else
403
+ # Try to extract text from MCP Content objects
404
+ if result.respond_to?(:text)
405
+ result.text
406
+ elsif result.respond_to?(:content)
407
+ result.content.is_a?(String) ? result.content : result.content.to_s
408
+ else
409
+ result.to_s
410
+ end
411
+ end
412
+
413
+ response = JSON.parse(json_str)
414
+
415
+ if response['success']
416
+ robot = response['current_robot']
417
+ stats = response['statistics']
418
+
419
+ puts
420
+ puts "Current Robot: #{robot['name']} (id=#{robot['id']})"
421
+ puts " Working memory: #{robot['memory_summary']['in_working_memory']} nodes"
422
+ puts " Total nodes: #{robot['memory_summary']['total_nodes']}"
423
+ puts " With embeddings: #{robot['memory_summary']['with_embeddings']}"
424
+ puts
425
+ puts 'Global Statistics:'
426
+ puts " Active nodes: #{stats['nodes']['active']}"
427
+ puts " Deleted nodes: #{stats['nodes']['deleted']}"
428
+ puts " Total tags: #{stats['tags']['total']}"
429
+ puts " Total robots: #{stats['robots']['total']}"
430
+ else
431
+ puts " Error: #{response['error']}"
432
+ end
433
+ puts
434
+ rescue JSON::ParserError => e
435
+ puts " Error parsing response: #{e.message}"
436
+ puts " Raw response: #{result.inspect[0..200]}"
437
+ puts
438
+ rescue StandardError => e
439
+ puts " Error: #{e.message}"
440
+ puts
441
+ end
442
+
443
+
444
+ def show_tags
445
+ puts "\nTags:"
446
+ puts '-' * 50
447
+
448
+ tags_tool = @tools.find { |t| t.name == 'ListTagsTool' }
449
+ unless tags_tool
450
+ puts ' ListTagsTool not available'
451
+ puts
452
+ return
453
+ end
454
+
455
+ result = tags_tool.call({})
456
+
457
+ # Extract JSON from MCP Content object
458
+ json_str = result.respond_to?(:text) ? result.text : result.to_s
459
+ response = JSON.parse(json_str)
460
+
461
+ if response['success']
462
+ tags = response['tags']
463
+ if tags.empty?
464
+ puts ' (no tags found)'
465
+ else
466
+ tags.each do |tag|
467
+ puts " #{tag['name']} (#{tag['node_count']} nodes)"
468
+ end
469
+ end
470
+ else
471
+ puts " Error: #{response['error']}"
472
+ end
473
+ puts
474
+ rescue JSON::ParserError => e
475
+ puts " Error parsing response: #{e.message}"
476
+ puts
477
+ rescue StandardError => e
478
+ puts " Error: #{e.message}"
479
+ puts
480
+ end
481
+
482
+
483
+ def clear_chat
484
+ @chat = RubyLLM.chat(
485
+ model: OLLAMA_MODEL,
486
+ provider: :ollama,
487
+ assume_model_exists: true
488
+ )
489
+ @chat.with_tools(*@tools)
490
+ setup_tool_callbacks
491
+
492
+ puts '[✓] Chat history cleared'
493
+ puts
494
+ end
495
+
496
+
497
+ def setup_tool_callbacks
498
+ @chat.on_tool_call do |tool_call|
499
+ puts "\n[Tool Call] #{tool_call.name}"
500
+ puts " Arguments: #{tool_call.arguments.inspect}"
501
+ end
502
+
503
+ @chat.on_tool_result do |tool_call, result|
504
+ # tool_call may be a Content object, so safely get the name
505
+ tool_name = tool_call.respond_to?(:name) ? tool_call.name : tool_call.class.name.split('::').last
506
+ puts "[Tool Result] #{tool_name}"
507
+ display_result = result.to_s
508
+ display_result = display_result[0..200] + '...' if display_result.length > 200
509
+ puts " Result: #{display_result}"
510
+ puts
511
+ end
512
+ end
513
+
514
+
515
+ def cleanup
516
+ @mcp_client&.close if @mcp_client.respond_to?(:close)
517
+ end
518
+ end
519
+
520
+ # Main entry point
521
+ begin
522
+ require 'ruby_llm/mcp'
523
+ rescue LoadError
524
+ warn 'Error: ruby_llm-mcp gem not found.'
525
+ warn 'Install it with: gem install ruby_llm-mcp'
526
+ exit 1
527
+ end
528
+
529
+ HTMMcpClient.new.run
data/lib/htm/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class HTM
4
- VERSION = "0.0.10"
4
+ VERSION = "0.0.11"
5
5
  end
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: htm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.10
4
+ version: 0.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
8
- bindir: exe
8
+ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
@@ -121,6 +121,20 @@ dependencies:
121
121
  - - ">="
122
122
  - !ruby/object:Gem::Version
123
123
  version: '0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: fast-mcp
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ type: :runtime
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
124
138
  - !ruby/object:Gem::Dependency
125
139
  name: rake
126
140
  requirement: !ruby/object:Gem::Requirement
@@ -177,6 +191,20 @@ dependencies:
177
191
  - - ">="
178
192
  - !ruby/object:Gem::Version
179
193
  version: '0'
194
+ - !ruby/object:Gem::Dependency
195
+ name: ruby_llm-mcp
196
+ requirement: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
201
+ type: :development
202
+ prerelease: false
203
+ version_requirements: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - ">="
206
+ - !ruby/object:Gem::Version
207
+ version: '0'
180
208
  - !ruby/object:Gem::Dependency
181
209
  name: yard
182
210
  requirement: !ruby/object:Gem::Requirement
@@ -213,7 +241,8 @@ description: |
213
241
  (Retrieval-Augmented Generation) techniques.
214
242
  email:
215
243
  - dvanhoozer@gmail.com
216
- executables: []
244
+ executables:
245
+ - htm_mcp.rb
217
246
  extensions: []
218
247
  extra_rdoc_files: []
219
248
  files:
@@ -249,6 +278,7 @@ files:
249
278
  - README.md
250
279
  - Rakefile
251
280
  - SETUP.md
281
+ - bin/htm_mcp.rb
252
282
  - config/database.yml
253
283
  - db/migrate/00001_enable_extensions.rb
254
284
  - db/migrate/00002_create_robots.rb
@@ -402,6 +432,7 @@ files:
402
432
  - examples/example_app/Rakefile
403
433
  - examples/example_app/app.rb
404
434
  - examples/file_loader_usage.rb
435
+ - examples/mcp_client.rb
405
436
  - examples/robot_groups/lib/robot_group.rb
406
437
  - examples/robot_groups/lib/working_memory_channel.rb
407
438
  - examples/robot_groups/multi_process.rb