@telvok/librarian-mcp 1.0.2 → 1.1.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.
package/dist/server.js CHANGED
@@ -5,6 +5,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextpro
5
5
  import { briefTool } from './tools/brief.js';
6
6
  import { recordTool } from './tools/record.js';
7
7
  import { adoptTool } from './tools/adopt.js';
8
+ import { markHitTool } from './tools/mark-hit.js';
8
9
  const server = new Server({
9
10
  name: 'librarian',
10
11
  version: '1.0.0',
@@ -32,6 +33,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
32
33
  description: adoptTool.description,
33
34
  inputSchema: adoptTool.inputSchema,
34
35
  },
36
+ {
37
+ name: markHitTool.name,
38
+ description: markHitTool.description,
39
+ inputSchema: markHitTool.inputSchema,
40
+ },
35
41
  ],
36
42
  };
37
43
  });
@@ -50,6 +56,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
50
56
  case 'adopt':
51
57
  result = await adoptTool.handler(args);
52
58
  break;
59
+ case 'mark_hit':
60
+ result = await markHitTool.handler(args);
61
+ break;
53
62
  default:
54
63
  throw new Error(`Unknown tool: ${name}`);
55
64
  }
@@ -5,6 +5,8 @@ export interface BriefEntry {
5
5
  preview: string;
6
6
  path: string;
7
7
  created: string;
8
+ hits: number;
9
+ last_hit: string | null;
8
10
  }
9
11
  export interface BriefResult {
10
12
  entries: BriefEntry[];
@@ -79,10 +79,9 @@ Examples:
79
79
  const searchTerm = query.toLowerCase();
80
80
  allEntries = allEntries.filter(entry => matchesSearch(entry, searchTerm));
81
81
  }
82
- // Sort by created date (most recent first)
83
- allEntries.sort((a, b) => {
84
- return new Date(b.created).getTime() - new Date(a.created).getTime();
85
- });
82
+ // Sort by blended score: 60% recency + 40% hits
83
+ // Entries that helped before bubble up, but new entries still surface
84
+ allEntries = rankEntries(allEntries);
86
85
  const total = allEntries.length;
87
86
  // Apply limit
88
87
  const entries = allEntries.slice(0, limit);
@@ -134,6 +133,8 @@ async function readEntry(filePath, libraryPath) {
134
133
  preview,
135
134
  path: path.relative(libraryPath, filePath),
136
135
  created: data.created || new Date().toISOString(),
136
+ hits: typeof data.hits === 'number' ? data.hits : 0,
137
+ last_hit: data.last_hit || null,
137
138
  };
138
139
  }
139
140
  catch {
@@ -159,3 +160,31 @@ function matchesSearch(entry, searchTerm) {
159
160
  }
160
161
  return false;
161
162
  }
163
+ // ============================================================================
164
+ // Smart Ranking
165
+ // ============================================================================
166
+ const RECENCY_WEIGHT = 0.6;
167
+ const HITS_WEIGHT = 0.4;
168
+ const RECENCY_DECAY_DAYS = 30; // Entries older than this get minimal recency score
169
+ function rankEntries(entries) {
170
+ if (entries.length === 0)
171
+ return entries;
172
+ const now = Date.now();
173
+ // Find max hits for normalization (avoid divide by zero)
174
+ const maxHits = Math.max(1, ...entries.map(e => e.hits));
175
+ // Calculate scores
176
+ const scored = entries.map(entry => {
177
+ // Recency score: 1.0 for today, decays over RECENCY_DECAY_DAYS
178
+ const ageMs = now - new Date(entry.created).getTime();
179
+ const ageDays = ageMs / (1000 * 60 * 60 * 24);
180
+ const recencyScore = Math.max(0, 1 - (ageDays / RECENCY_DECAY_DAYS));
181
+ // Hits score: normalized 0-1 against max hits in library
182
+ const hitsScore = entry.hits / maxHits;
183
+ // Blended score
184
+ const score = (RECENCY_WEIGHT * recencyScore) + (HITS_WEIGHT * hitsScore);
185
+ return { entry, score };
186
+ });
187
+ // Sort by score descending
188
+ scored.sort((a, b) => b.score - a.score);
189
+ return scored.map(s => s.entry);
190
+ }
@@ -1,3 +1,4 @@
1
1
  export { briefTool } from './brief.js';
2
2
  export { recordTool } from './record.js';
3
3
  export { adoptTool } from './adopt.js';
4
+ export { markHitTool } from './mark-hit.js';
@@ -1,3 +1,4 @@
1
1
  export { briefTool } from './brief.js';
2
2
  export { recordTool } from './record.js';
3
3
  export { adoptTool } from './adopt.js';
4
+ export { markHitTool } from './mark-hit.js';
@@ -0,0 +1,20 @@
1
+ export interface MarkHitResult {
2
+ success: boolean;
3
+ path: string;
4
+ hits: number;
5
+ }
6
+ export declare const markHitTool: {
7
+ name: string;
8
+ description: string;
9
+ inputSchema: {
10
+ type: "object";
11
+ properties: {
12
+ path: {
13
+ type: string;
14
+ description: string;
15
+ };
16
+ };
17
+ required: string[];
18
+ };
19
+ handler(args: unknown): Promise<MarkHitResult>;
20
+ };
@@ -0,0 +1,71 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import matter from 'gray-matter';
4
+ import { getLibraryPath } from '../library/storage.js';
5
+ // ============================================================================
6
+ // Tool Definition
7
+ // ============================================================================
8
+ export const markHitTool = {
9
+ name: 'mark_hit',
10
+ description: `Mark a library entry as helpful - call this when knowledge from the library helped solve a problem.
11
+
12
+ When an entry from brief() actually helped you complete a task or make a decision,
13
+ call mark_hit() on it. This helps the library learn which entries are most useful.
14
+
15
+ Entries with more hits bubble up in future brief() results.
16
+
17
+ Fire and forget - call it and move on.
18
+
19
+ Example:
20
+ - mark_hit({ path: "local/stripe-webhooks-need-idempotency.md" })`,
21
+ inputSchema: {
22
+ type: 'object',
23
+ properties: {
24
+ path: {
25
+ type: 'string',
26
+ description: 'Path to the entry that helped (from brief() results)',
27
+ },
28
+ },
29
+ required: ['path'],
30
+ },
31
+ async handler(args) {
32
+ const { path: entryPath } = args;
33
+ if (!entryPath) {
34
+ throw new Error('path is required');
35
+ }
36
+ const libraryPath = getLibraryPath();
37
+ // Resolve the full path
38
+ let fullPath;
39
+ if (path.isAbsolute(entryPath)) {
40
+ fullPath = entryPath;
41
+ }
42
+ else {
43
+ fullPath = path.join(libraryPath, entryPath);
44
+ }
45
+ // Read existing file
46
+ let content;
47
+ try {
48
+ content = await fs.readFile(fullPath, 'utf-8');
49
+ }
50
+ catch {
51
+ throw new Error(`Entry not found: ${entryPath}`);
52
+ }
53
+ // Parse frontmatter
54
+ const { data, content: body } = matter(content);
55
+ // Increment hits
56
+ const currentHits = typeof data.hits === 'number' ? data.hits : 0;
57
+ const newHits = currentHits + 1;
58
+ // Update frontmatter
59
+ data.hits = newHits;
60
+ data.last_hit = new Date().toISOString();
61
+ // Rebuild file content
62
+ const updatedContent = matter.stringify(body, data);
63
+ // Write back
64
+ await fs.writeFile(fullPath, updatedContent, 'utf-8');
65
+ return {
66
+ success: true,
67
+ path: entryPath,
68
+ hits: newHits,
69
+ };
70
+ },
71
+ };
@@ -36,9 +36,11 @@ Quick:
36
36
 
37
37
  Rich:
38
38
  - record({
39
+ intent: "Setting up GitHub org for Telvok",
39
40
  insight: "GitHub org names are first-come-first-served regardless of domain ownership",
40
41
  context: "GitHub, npm, branding",
41
- reasoning: "We owned telvok.com but someone squatted telvok org years ago"
42
+ reasoning: "We owned telvok.com but someone squatted telvok org years ago",
43
+ example: "Had to use telvokdev instead of telvok"
42
44
  })`,
43
45
  inputSchema: {
44
46
  type: 'object',
@@ -104,6 +106,8 @@ Rich:
104
106
  frontmatterLines.push(`created: "${created}"`);
105
107
  frontmatterLines.push(`updated: "${created}"`);
106
108
  frontmatterLines.push('source: "local"');
109
+ frontmatterLines.push('hits: 0');
110
+ frontmatterLines.push('last_hit: null');
107
111
  frontmatterLines.push('---');
108
112
  // Build body
109
113
  const bodyLines = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telvok/librarian-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Knowledge capture MCP server - remember what you learn with AI",
5
5
  "type": "module",
6
6
  "main": "dist/server.js",