@@ -9,12 +9,13 @@ import { q, all_async } from '../core/db'
99import { getEmbeddingInfo } from '../memory/embed'
1010import { j , p } from '../utils'
1111import type { sector_type , mem_row , rpc_err_code } from '../core/types'
12+ import { update_user_summary } from '../memory/user_summary'
1213
1314const sec_enum = z . enum ( [ 'episodic' , 'semantic' , 'procedural' , 'emotional' , 'reflective' ] as const )
1415
1516const trunc = ( val : string , max = 200 ) => val . length <= max ? val : `${ val . slice ( 0 , max ) . trimEnd ( ) } ...`
1617
17- const build_mem_snap = ( row : mem_row ) => ( { id : row . id , primary_sector : row . primary_sector , salience : Number ( row . salience . toFixed ( 3 ) ) , last_seen_at : row . last_seen_at , content_preview : trunc ( row . content , 240 ) } )
18+ const build_mem_snap = ( row : mem_row ) => ( { id : row . id , primary_sector : row . primary_sector , salience : Number ( row . salience . toFixed ( 3 ) ) , last_seen_at : row . last_seen_at , user_id : row . user_id , content_preview : trunc ( row . content , 240 ) } )
1819
1920const fmt_matches = ( matches : Awaited < ReturnType < typeof hsg_query > > ) => matches . map ( ( m : any , idx : any ) => {
2021 const prev = trunc ( m . content . replace ( / \s + / g, ' ' ) . trim ( ) , 200 )
@@ -36,16 +37,24 @@ const send_err = (res: ServerResponse, code: rpc_err_code, msg: string, id: numb
3637 }
3738}
3839
40+ const uid = ( val ?: string | null ) => val ?. trim ( ) ? val . trim ( ) : undefined
41+
3942export const create_mcp_srv = ( ) => {
4043 const srv = new McpServer ( { name : 'openmemory-mcp' , version : '2.1.0' , protocolVersion : '2025-06-18' } , { capabilities : { tools : { } , resources : { } , logging : { } } } )
4144
4245 srv . tool ( 'openmemory.query' , 'Run a semantic retrieval against OpenMemory' , {
4346 query : z . string ( ) . min ( 1 , 'query text is required' ) . describe ( 'Free-form search text' ) ,
4447 k : z . number ( ) . int ( ) . min ( 1 ) . max ( 32 ) . default ( 8 ) . describe ( 'Maximum results to return' ) ,
4548 sector : sec_enum . optional ( ) . describe ( 'Restrict search to a specific sector' ) ,
46- min_salience : z . number ( ) . min ( 0 ) . max ( 1 ) . optional ( ) . describe ( 'Minimum salience threshold' )
47- } , async ( { query, k, sector, min_salience } ) => {
48- const flt = { sectors : sector ? [ sector as sector_type ] : undefined , minSalience : min_salience }
49+ min_salience : z . number ( ) . min ( 0 ) . max ( 1 ) . optional ( ) . describe ( 'Minimum salience threshold' ) ,
50+ user_id : z . string ( ) . trim ( ) . min ( 1 ) . optional ( ) . describe ( 'Isolate results to a specific user identifier' )
51+ } , async ( { query, k, sector, min_salience, user_id } ) => {
52+ const u = uid ( user_id )
53+ const flt = sector || min_salience !== undefined || u ? {
54+ ...( sector ? { sectors : [ sector as sector_type ] } : { } ) ,
55+ ...( min_salience !== undefined ? { minSalience : min_salience } : { } ) ,
56+ ...( u ? { user_id : u } : { } )
57+ } : undefined
4958 const matches = await hsg_query ( query , k ?? 8 , flt )
5059 const summ = matches . length ? fmt_matches ( matches ) : 'No memories matched the supplied query.'
5160 const pay = matches . map ( ( m : any ) => ( { id : m . id , score : Number ( m . score . toFixed ( 4 ) ) , primary_sector : m . primary_sector , sectors : m . sectors , salience : Number ( m . salience . toFixed ( 4 ) ) , last_seen_at : m . last_seen_at , path : m . path , content : m . content } ) )
@@ -55,11 +64,15 @@ export const create_mcp_srv = () => {
5564 srv . tool ( 'openmemory.store' , 'Persist new content into OpenMemory' , {
5665 content : z . string ( ) . min ( 1 ) . describe ( 'Raw memory text to store' ) ,
5766 tags : z . array ( z . string ( ) ) . optional ( ) . describe ( 'Optional tag list' ) ,
58- metadata : z . record ( z . any ( ) ) . optional ( ) . describe ( 'Arbitrary metadata blob' )
59- } , async ( { content, tags, metadata } ) => {
60- const res = await add_hsg_memory ( content , j ( tags || [ ] ) , metadata )
61- const txt = `Stored memory ${ res . id } (primary=${ res . primary_sector } ) across sectors: ${ res . sectors . join ( ', ' ) } `
62- return { content : [ { type : 'text' , text : txt } , { type : 'text' , text : JSON . stringify ( { id : res . id , primary_sector : res . primary_sector , sectors : res . sectors } , null , 2 ) } ] }
67+ metadata : z . record ( z . any ( ) ) . optional ( ) . describe ( 'Arbitrary metadata blob' ) ,
68+ user_id : z . string ( ) . trim ( ) . min ( 1 ) . optional ( ) . describe ( 'Associate the memory with a specific user identifier' )
69+ } , async ( { content, tags, metadata, user_id } ) => {
70+ const u = uid ( user_id )
71+ const res = await add_hsg_memory ( content , j ( tags || [ ] ) , metadata , u )
72+ if ( u ) update_user_summary ( u ) . catch ( err => console . error ( '[MCP] user summary update failed:' , err ) )
73+ const txt = `Stored memory ${ res . id } (primary=${ res . primary_sector } ) across sectors: ${ res . sectors . join ( ', ' ) } ${ u ? ` [user=${ u } ]` : '' } `
74+ const payload = { id : res . id , primary_sector : res . primary_sector , sectors : res . sectors , user_id : u ?? null }
75+ return { content : [ { type : 'text' , text : txt } , { type : 'text' , text : JSON . stringify ( payload , null , 2 ) } ] }
6376 } )
6477
6578 srv . tool ( 'openmemory.reinforce' , 'Boost salience for an existing memory' , {
@@ -72,25 +85,46 @@ export const create_mcp_srv = () => {
7285
7386 srv . tool ( 'openmemory.list' , 'List recent memories for quick inspection' , {
7487 limit : z . number ( ) . int ( ) . min ( 1 ) . max ( 50 ) . default ( 10 ) . describe ( 'Number of memories to return' ) ,
75- sector : sec_enum . optional ( ) . describe ( 'Optionally limit to a sector' )
76- } , async ( { limit, sector } ) => {
77- const rows : mem_row [ ] = sector ? await q . all_mem_by_sector . all ( sector , limit ?? 10 , 0 ) : await q . all_mem . all ( limit ?? 10 , 0 )
88+ sector : sec_enum . optional ( ) . describe ( 'Optionally limit to a sector' ) ,
89+ user_id : z . string ( ) . trim ( ) . min ( 1 ) . optional ( ) . describe ( 'Restrict results to a specific user identifier' )
90+ } , async ( { limit, sector, user_id } ) => {
91+ const u = uid ( user_id )
92+ let rows : mem_row [ ]
93+ if ( u ) {
94+ const all = await q . all_mem_by_user . all ( u , limit ?? 10 , 0 )
95+ rows = sector ? all . filter ( row => row . primary_sector === sector ) : all
96+ } else {
97+ rows = sector ? await q . all_mem_by_sector . all ( sector , limit ?? 10 , 0 ) : await q . all_mem . all ( limit ?? 10 , 0 )
98+ }
7899 const items = rows . map ( row => ( { ...build_mem_snap ( row ) , tags : p ( row . tags || '[]' ) as string [ ] , metadata : p ( row . meta || '{}' ) as Record < string , unknown > } ) )
79- const lns = items . map ( ( item , idx ) => {
80- const tag_str = item . tags . length ? ` tags=${ item . tags . join ( ', ' ) } ` : ''
81- return `${ idx + 1 } . [${ item . primary_sector } ] salience=${ item . salience } id=${ item . id } ${ tag_str } \n${ item . content_preview } `
82- } )
100+ const lns = items . map ( ( item , idx ) => `${ idx + 1 } . [${ item . primary_sector } ] salience=${ item . salience } id=${ item . id } ${ item . tags . length ? ` tags=${ item . tags . join ( ', ' ) } ` : '' } ${ item . user_id ? ` user=${ item . user_id } ` : '' } \n${ item . content_preview } ` )
83101 return { content : [ { type : 'text' , text : lns . join ( '\n\n' ) || 'No memories stored yet.' } , { type : 'text' , text : JSON . stringify ( { items } , null , 2 ) } ] }
84102 } )
85103
86104 srv . tool ( 'openmemory.get' , 'Fetch a single memory by identifier' , {
87105 id : z . string ( ) . min ( 1 ) . describe ( 'Memory identifier to load' ) ,
88- include_vectors : z . boolean ( ) . default ( false ) . describe ( 'Include sector vector metadata' )
89- } , async ( { id, include_vectors } ) => {
106+ include_vectors : z . boolean ( ) . default ( false ) . describe ( 'Include sector vector metadata' ) ,
107+ user_id : z . string ( ) . trim ( ) . min ( 1 ) . optional ( ) . describe ( 'Validate ownership against a specific user identifier' )
108+ } , async ( { id, include_vectors, user_id } ) => {
109+ const u = uid ( user_id )
90110 const mem = await q . get_mem . get ( id )
91111 if ( ! mem ) return { content : [ { type : 'text' , text : `Memory ${ id } not found.` } ] }
112+ if ( u && mem . user_id !== u ) return { content : [ { type : 'text' , text : `Memory ${ id } not found for user ${ u } .` } ] }
92113 const vecs = include_vectors ? await q . get_vecs_by_id . all ( id ) : [ ]
93- const pay = { id : mem . id , content : mem . content , primary_sector : mem . primary_sector , salience : mem . salience , decay_lambda : mem . decay_lambda , created_at : mem . created_at , updated_at : mem . updated_at , last_seen_at : mem . last_seen_at , tags : p ( mem . tags || '[]' ) , metadata : p ( mem . meta || '{}' ) , sectors : include_vectors ? vecs . map ( v => v . sector ) : undefined }
114+ const pay = {
115+ id : mem . id ,
116+ content : mem . content ,
117+ primary_sector : mem . primary_sector ,
118+ salience : mem . salience ,
119+ decay_lambda : mem . decay_lambda ,
120+ created_at : mem . created_at ,
121+ updated_at : mem . updated_at ,
122+ last_seen_at : mem . last_seen_at ,
123+ user_id : mem . user_id ,
124+ tags : p ( mem . tags || '[]' ) ,
125+ metadata : p ( mem . meta || '{}' ) ,
126+ sectors : include_vectors ? vecs . map ( v => v . sector ) : undefined
127+ }
94128 return { content : [ { type : 'text' , text : JSON . stringify ( pay , null , 2 ) } ] }
95129 } )
96130
@@ -161,4 +195,4 @@ export const start_mcp_stdio = async () => {
161195
162196if ( typeof require !== 'undefined' && require . main === module ) {
163197 void start_mcp_stdio ( ) . catch ( error => { console . error ( '[MCP] STDIO startup failed:' , error ) ; process . exitCode = 1 } )
164- }
198+ }
0 commit comments