llmemory 0.1.10 → 0.1.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.
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module MCP
5
+ module Tools
6
+ class MemorySearch < ::MCP::Tool
7
+ description "Search through user's memory for relevant information. Returns facts, observations, and context matching the query."
8
+
9
+ input_schema(
10
+ properties: {
11
+ query: { type: "string", description: "Search query to find relevant memories" },
12
+ user_id: { type: "string", description: "User identifier" },
13
+ search_type: {
14
+ type: "string",
15
+ enum: ["all", "short_term", "long_term"],
16
+ description: "Where to search: all (default), short_term, or long_term"
17
+ },
18
+ max_results: { type: "integer", description: "Maximum results (default: 10)" }
19
+ },
20
+ required: ["query", "user_id"]
21
+ )
22
+
23
+ class << self
24
+ def call(query:, user_id:, search_type: "all", max_results: 10, server_context: nil)
25
+ results = []
26
+ search_type = (search_type || "all").downcase
27
+ max_results = max_results || 10
28
+
29
+ if search_type == "all" || search_type == "short_term"
30
+ results.concat(search_short_term(user_id, query, max_results))
31
+ end
32
+
33
+ if search_type == "all" || search_type == "long_term"
34
+ results.concat(search_long_term(user_id, query, max_results))
35
+ end
36
+
37
+ ::MCP::Tool::Response.new([{
38
+ type: "text",
39
+ text: format_results(results.first(max_results))
40
+ }])
41
+ rescue => e
42
+ ::MCP::Tool::Response.new([{
43
+ type: "text",
44
+ text: "Error searching memory: #{e.message}"
45
+ }], error: true)
46
+ end
47
+
48
+ private
49
+
50
+ def search_short_term(user_id, query, limit)
51
+ store = build_short_term_store
52
+ sessions = store.list_sessions(user_id: user_id)
53
+ results = []
54
+ query_lower = query.downcase
55
+
56
+ sessions.each do |session_id|
57
+ state = store.load(user_id, session_id)
58
+ next unless state.is_a?(Hash)
59
+ messages = state[:messages] || state["messages"] || []
60
+ messages.each do |m|
61
+ content = (m[:content] || m["content"]).to_s
62
+ if content.downcase.include?(query_lower)
63
+ results << {
64
+ type: "short_term",
65
+ session_id: session_id,
66
+ role: m[:role] || m["role"],
67
+ content: content
68
+ }
69
+ end
70
+ end
71
+ end
72
+
73
+ results.first(limit)
74
+ end
75
+
76
+ def search_long_term(user_id, query, limit)
77
+ storage = build_long_term_storage
78
+ results = []
79
+
80
+ items = storage.search_items(user_id, query)
81
+ items.first(limit).each do |item|
82
+ results << {
83
+ type: "long_term_fact",
84
+ category: item[:category] || item["category"],
85
+ content: item[:content] || item["content"],
86
+ created_at: item[:created_at] || item["created_at"]
87
+ }
88
+ end
89
+
90
+ results
91
+ end
92
+
93
+ def build_short_term_store
94
+ case Llmemory.configuration.short_term_store.to_sym
95
+ when :memory then ShortTerm::Stores::MemoryStore.new
96
+ when :redis then ShortTerm::Stores::RedisStore.new
97
+ when :postgres then ShortTerm::Stores::PostgresStore.new
98
+ when :active_record, :activerecord
99
+ require_relative "../../short_term/stores/active_record_store"
100
+ ShortTerm::Stores::ActiveRecordStore.new
101
+ else
102
+ ShortTerm::Stores::MemoryStore.new
103
+ end
104
+ end
105
+
106
+ def build_long_term_storage
107
+ LongTerm::FileBased::Storages.build
108
+ end
109
+
110
+ def format_results(results)
111
+ return "No memories found matching the query." if results.empty?
112
+
113
+ output = ["Found #{results.size} memories:\n"]
114
+ results.each_with_index do |r, i|
115
+ case r[:type]
116
+ when "short_term"
117
+ output << "#{i + 1}. [Session: #{r[:session_id]}] [#{r[:role]}] #{truncate(r[:content], 200)}"
118
+ when "long_term_fact"
119
+ cat_info = r[:category] ? "[#{r[:category]}]" : ""
120
+ output << "#{i + 1}. #{cat_info} #{truncate(r[:content], 200)}"
121
+ end
122
+ end
123
+ output.join("\n")
124
+ end
125
+
126
+ def truncate(text, max_length)
127
+ return text if text.length <= max_length
128
+ "#{text[0, max_length]}..."
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module MCP
5
+ module Tools
6
+ class MemoryStats < ::MCP::Tool
7
+ description "Get memory statistics for a user including message counts, fact counts, and categories."
8
+
9
+ input_schema(
10
+ properties: {
11
+ user_id: { type: "string", description: "User identifier" }
12
+ },
13
+ required: ["user_id"]
14
+ )
15
+
16
+ class << self
17
+ def call(user_id:, server_context: nil)
18
+ stats = {
19
+ user_id: user_id,
20
+ short_term: {},
21
+ long_term: {}
22
+ }
23
+
24
+ # Short-term stats
25
+ store = build_short_term_store
26
+ sessions = store.list_sessions(user_id: user_id)
27
+ total_messages = 0
28
+ sessions.each do |session_id|
29
+ state = store.load(user_id, session_id)
30
+ next unless state.is_a?(Hash)
31
+ messages = state[:messages] || state["messages"] || []
32
+ total_messages += messages.size
33
+ end
34
+ stats[:short_term] = {
35
+ sessions: sessions.size,
36
+ total_messages: total_messages
37
+ }
38
+
39
+ # Long-term stats
40
+ storage = build_long_term_storage
41
+ begin
42
+ item_count = storage.count_items(user_id: user_id)
43
+ categories = storage.list_categories(user_id)
44
+ resources = storage.list_resources(user_id: user_id)
45
+ stats[:long_term] = {
46
+ facts: item_count,
47
+ categories: categories.size,
48
+ category_names: categories,
49
+ resources: resources.size
50
+ }
51
+ rescue => e
52
+ stats[:long_term] = { error: e.message }
53
+ end
54
+
55
+ ::MCP::Tool::Response.new([{
56
+ type: "text",
57
+ text: format_stats(stats)
58
+ }])
59
+ rescue => e
60
+ ::MCP::Tool::Response.new([{
61
+ type: "text",
62
+ text: "Error getting stats: #{e.message}"
63
+ }], error: true)
64
+ end
65
+
66
+ private
67
+
68
+ def build_short_term_store
69
+ case Llmemory.configuration.short_term_store.to_sym
70
+ when :memory then ShortTerm::Stores::MemoryStore.new
71
+ when :redis then ShortTerm::Stores::RedisStore.new
72
+ when :postgres then ShortTerm::Stores::PostgresStore.new
73
+ when :active_record, :activerecord
74
+ require_relative "../../short_term/stores/active_record_store"
75
+ ShortTerm::Stores::ActiveRecordStore.new
76
+ else
77
+ ShortTerm::Stores::MemoryStore.new
78
+ end
79
+ end
80
+
81
+ def build_long_term_storage
82
+ LongTerm::FileBased::Storages.build
83
+ end
84
+
85
+ def format_stats(stats)
86
+ output = ["Memory Statistics for user '#{stats[:user_id]}':\n"]
87
+
88
+ output << "SHORT-TERM MEMORY:"
89
+ output << " Sessions: #{stats[:short_term][:sessions]}"
90
+ output << " Total messages: #{stats[:short_term][:total_messages]}"
91
+ output << ""
92
+
93
+ output << "LONG-TERM MEMORY:"
94
+ if stats[:long_term][:error]
95
+ output << " Error: #{stats[:long_term][:error]}"
96
+ else
97
+ output << " Facts stored: #{stats[:long_term][:facts]}"
98
+ output << " Categories: #{stats[:long_term][:categories]}"
99
+ if stats[:long_term][:category_names]&.any?
100
+ output << " Category names: #{stats[:long_term][:category_names].join(', ')}"
101
+ end
102
+ output << " Resources: #{stats[:long_term][:resources]}"
103
+ end
104
+
105
+ output.join("\n")
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module MCP
5
+ module Tools
6
+ class MemoryTimeline < ::MCP::Tool
7
+ description "Get chronological timeline of recent memories and interactions for a user."
8
+
9
+ input_schema(
10
+ properties: {
11
+ user_id: { type: "string", description: "User identifier" },
12
+ hours: { type: "integer", description: "Hours to look back (default: 24)" },
13
+ include_messages: { type: "boolean", description: "Include short-term messages (default: true)" }
14
+ },
15
+ required: ["user_id"]
16
+ )
17
+
18
+ class << self
19
+ def call(user_id:, hours: nil, include_messages: nil, server_context: nil)
20
+ hours = hours || 24
21
+ include_msgs = include_messages.nil? ? true : include_messages
22
+
23
+ timeline = []
24
+
25
+ # Get recent items from long-term memory
26
+ storage = build_long_term_storage
27
+ begin
28
+ items = storage.get_items_since(user_id, hours: hours)
29
+ items.each do |item|
30
+ timeline << {
31
+ type: "fact",
32
+ timestamp: item[:created_at] || item["created_at"],
33
+ category: item[:category] || item["category"],
34
+ content: item[:content] || item["content"]
35
+ }
36
+ end
37
+ rescue NotImplementedError
38
+ # Some storages may not implement get_items_since
39
+ end
40
+
41
+ # Get recent messages from short-term
42
+ if include_msgs
43
+ store = build_short_term_store
44
+ sessions = store.list_sessions(user_id: user_id)
45
+ sessions.each do |session_id|
46
+ state = store.load(user_id, session_id)
47
+ next unless state.is_a?(Hash)
48
+ messages = state[:messages] || state["messages"] || []
49
+ messages.each_with_index do |m, idx|
50
+ timeline << {
51
+ type: "message",
52
+ timestamp: state[:updated_at] || Time.now,
53
+ session_id: session_id,
54
+ role: m[:role] || m["role"],
55
+ content: m[:content] || m["content"],
56
+ order: idx
57
+ }
58
+ end
59
+ end
60
+ end
61
+
62
+ # Sort by timestamp (most recent first)
63
+ timeline.sort_by! { |t| t[:timestamp].to_s }.reverse!
64
+
65
+ ::MCP::Tool::Response.new([{
66
+ type: "text",
67
+ text: format_timeline(timeline, hours)
68
+ }])
69
+ rescue => e
70
+ ::MCP::Tool::Response.new([{
71
+ type: "text",
72
+ text: "Error getting timeline: #{e.message}"
73
+ }], error: true)
74
+ end
75
+
76
+ private
77
+
78
+ def build_short_term_store
79
+ case Llmemory.configuration.short_term_store.to_sym
80
+ when :memory then ShortTerm::Stores::MemoryStore.new
81
+ when :redis then ShortTerm::Stores::RedisStore.new
82
+ when :postgres then ShortTerm::Stores::PostgresStore.new
83
+ when :active_record, :activerecord
84
+ require_relative "../../short_term/stores/active_record_store"
85
+ ShortTerm::Stores::ActiveRecordStore.new
86
+ else
87
+ ShortTerm::Stores::MemoryStore.new
88
+ end
89
+ end
90
+
91
+ def build_long_term_storage
92
+ LongTerm::FileBased::Storages.build
93
+ end
94
+
95
+ def format_timeline(timeline, hours)
96
+ return "No activity in the last #{hours} hours." if timeline.empty?
97
+
98
+ output = ["Timeline (last #{hours} hours):\n"]
99
+ timeline.first(20).each do |entry|
100
+ case entry[:type]
101
+ when "fact"
102
+ cat_info = entry[:category] ? "[#{entry[:category]}]" : ""
103
+ output << "- [FACT] #{cat_info} #{truncate(entry[:content], 150)}"
104
+ when "message"
105
+ output << "- [MSG #{entry[:session_id]}] [#{entry[:role]}] #{truncate(entry[:content], 150)}"
106
+ end
107
+ end
108
+ output << "\n(Showing #{[timeline.size, 20].min} of #{timeline.size} entries)"
109
+ output.join("\n")
110
+ end
111
+
112
+ def truncate(text, max_length)
113
+ return text.to_s if text.to_s.length <= max_length
114
+ "#{text.to_s[0, max_length]}..."
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module MCP
5
+ module Tools
6
+ class MemoryTimelineContext < ::MCP::Tool
7
+ description "Get temporal context around a specific memory. Shows N events before and after a given item or timestamp."
8
+
9
+ input_schema(
10
+ properties: {
11
+ user_id: { type: "string", description: "User identifier" },
12
+ item_id: { type: "string", description: "ID of the item to get context for (e.g., from search results)" },
13
+ timestamp: { type: "string", description: "ISO timestamp to get context around (alternative to item_id)" },
14
+ before: { type: "integer", description: "Number of items before the target (default: 5)" },
15
+ after: { type: "integer", description: "Number of items after the target (default: 5)" }
16
+ },
17
+ required: ["user_id"]
18
+ )
19
+
20
+ class << self
21
+ def call(user_id:, item_id: nil, timestamp: nil, before: nil, after: nil, server_context: nil)
22
+ before_count = before || 5
23
+ after_count = after || 5
24
+
25
+ reference = item_id || timestamp
26
+ unless reference
27
+ return ::MCP::Tool::Response.new([{
28
+ type: "text",
29
+ text: "Error: Either item_id or timestamp must be provided"
30
+ }], error: true)
31
+ end
32
+
33
+ storage = build_storage
34
+ result = storage.get_items_around(user_id, reference, before: before_count, after: after_count)
35
+
36
+ ::MCP::Tool::Response.new([{
37
+ type: "text",
38
+ text: format_context(result, reference)
39
+ }])
40
+ rescue => e
41
+ ::MCP::Tool::Response.new([{
42
+ type: "text",
43
+ text: "Error getting timeline context: #{e.message}"
44
+ }], error: true)
45
+ end
46
+
47
+ private
48
+
49
+ def build_storage
50
+ if Llmemory.configuration.long_term_type.to_s == "graph_based"
51
+ LongTerm::GraphBased::Storages.build
52
+ else
53
+ LongTerm::FileBased::Storages.build
54
+ end
55
+ end
56
+
57
+ def format_context(result, reference)
58
+ output = []
59
+ output << "Timeline Context around '#{reference}':\n"
60
+
61
+ if result[:before].empty? && result[:target].nil? && result[:after].empty?
62
+ return "No memories found around reference '#{reference}'"
63
+ end
64
+
65
+ # Before section
66
+ output << "BEFORE (#{result[:before].size} items):"
67
+ if result[:before].empty?
68
+ output << " (no earlier items)"
69
+ else
70
+ result[:before].each do |item|
71
+ output << format_item(item)
72
+ end
73
+ end
74
+
75
+ # Target section
76
+ output << "\nTARGET:"
77
+ if result[:target]
78
+ output << format_item(result[:target], highlight: true)
79
+ else
80
+ output << " (target not found)"
81
+ end
82
+
83
+ # After section
84
+ output << "\nAFTER (#{result[:after].size} items):"
85
+ if result[:after].empty?
86
+ output << " (no later items)"
87
+ else
88
+ result[:after].each do |item|
89
+ output << format_item(item)
90
+ end
91
+ end
92
+
93
+ output.join("\n")
94
+ end
95
+
96
+ def format_item(item, highlight: false)
97
+ prefix = highlight ? ">>> " : " - "
98
+ timestamp = format_timestamp(item)
99
+ content = extract_content(item)
100
+ category = extract_category(item)
101
+
102
+ cat_info = category ? "[#{category}] " : ""
103
+ "#{prefix}[#{timestamp}] #{cat_info}#{truncate(content, 120)}"
104
+ end
105
+
106
+ def format_timestamp(item)
107
+ ts = item[:created_at] || item["created_at"] || item.created_at rescue nil
108
+ return "unknown" unless ts
109
+ ts.respond_to?(:strftime) ? ts.strftime("%Y-%m-%d %H:%M") : ts.to_s[0, 16]
110
+ end
111
+
112
+ def extract_content(item)
113
+ # Handle both hash and object representations
114
+ if item.respond_to?(:content)
115
+ item.content
116
+ elsif item.respond_to?(:predicate)
117
+ # Graph-based edge
118
+ "#{item.subject_id} -> #{item.predicate} -> #{item.target_id}"
119
+ else
120
+ item[:content] || item["content"] || item.to_s
121
+ end
122
+ end
123
+
124
+ def extract_category(item)
125
+ if item.respond_to?(:category)
126
+ item.category
127
+ else
128
+ item[:category] || item["category"]
129
+ end
130
+ end
131
+
132
+ def truncate(text, max_length)
133
+ return text.to_s if text.to_s.length <= max_length
134
+ "#{text.to_s[0, max_length]}..."
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mcp/server"
4
+
5
+ module Llmemory
6
+ module MCP
7
+ end
8
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Llmemory
4
- VERSION = "0.1.10"
4
+ VERSION = "0.1.11"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llmemory
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.10
4
+ version: 0.1.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - llmemory
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: mcp
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.6'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.6'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: rspec
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -71,6 +85,7 @@ email:
71
85
  - ''
72
86
  executables:
73
87
  - llmemory
88
+ - llmemory-mcp
74
89
  extensions: []
75
90
  extra_rdoc_files: []
76
91
  files:
@@ -94,6 +109,7 @@ files:
94
109
  - app/views/llmemory/dashboard/users/show.html.erb
95
110
  - config/routes.rb
96
111
  - exe/llmemory
112
+ - exe/llmemory-mcp
97
113
  - lib/generators/llmemory/install/install_generator.rb
98
114
  - lib/generators/llmemory/install/templates/create_llmemory_tables.rb
99
115
  - lib/llmemory.rb
@@ -106,6 +122,7 @@ files:
106
122
  - lib/llmemory/cli/commands/long_term/graph.rb
107
123
  - lib/llmemory/cli/commands/long_term/nodes.rb
108
124
  - lib/llmemory/cli/commands/long_term/resources.rb
125
+ - lib/llmemory/cli/commands/mcp.rb
109
126
  - lib/llmemory/cli/commands/search.rb
110
127
  - lib/llmemory/cli/commands/short_term.rb
111
128
  - lib/llmemory/cli/commands/stats.rb
@@ -150,6 +167,18 @@ files:
150
167
  - lib/llmemory/maintenance/reindexer.rb
151
168
  - lib/llmemory/maintenance/runner.rb
152
169
  - lib/llmemory/maintenance/summarizer.rb
170
+ - lib/llmemory/mcp.rb
171
+ - lib/llmemory/mcp/authentication.rb
172
+ - lib/llmemory/mcp/server.rb
173
+ - lib/llmemory/mcp/tools/memory_add_message.rb
174
+ - lib/llmemory/mcp/tools/memory_consolidate.rb
175
+ - lib/llmemory/mcp/tools/memory_info.rb
176
+ - lib/llmemory/mcp/tools/memory_retrieve.rb
177
+ - lib/llmemory/mcp/tools/memory_save.rb
178
+ - lib/llmemory/mcp/tools/memory_search.rb
179
+ - lib/llmemory/mcp/tools/memory_stats.rb
180
+ - lib/llmemory/mcp/tools/memory_timeline.rb
181
+ - lib/llmemory/mcp/tools/memory_timeline_context.rb
153
182
  - lib/llmemory/memory.rb
154
183
  - lib/llmemory/retrieval.rb
155
184
  - lib/llmemory/retrieval/context_assembler.rb