@pragmatic-growth/memory-mcp 2.1.1 → 2.3.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/.npmrc.backup ADDED
@@ -0,0 +1,2 @@
1
+ @pragmatic-growth:registry=https://registry.npmjs.org/
2
+ //registry.npmjs.org/:_authToken=${NPM_TOKEN}
package/README.md CHANGED
@@ -41,14 +41,11 @@ For clients that only support stdio (Raycast, local tools):
41
41
  Both transports support two operating modes:
42
42
 
43
43
  ### Read-Only Mode (Default)
44
- Safe for general use - only search and read operations:
45
- - `search_knowledge` - Semantic search with relevance scores
46
- - `answer_question` - RAG with auto-generation
47
- - `get_article` - Get article by ID
48
- - `list_articles` - Browse articles with category filter
49
- - `list_categories` - List categories with article counts
50
- - `log_unanswered` - Flag knowledge gaps
51
- - `health_check` - System status
44
+ Safe for general use - search, read, and conversation tracking:
45
+ - **Core Knowledge**: search, get content, answer questions, list content
46
+ - **Knowledge Graph**: find related articles, get entities, search entities
47
+ - **Episodic Memory**: start/track conversations, get session context
48
+ - **Q&A**: add questions for later answering
52
49
 
53
50
  ### Full Mode (with `--full` flag)
54
51
  Includes all read-only tools PLUS write operations:
@@ -113,28 +110,53 @@ MCP_API_KEY=your-key memory-mcp --full
113
110
 
114
111
  Note: The `--full` CLI flag takes precedence over `MCP_MODE` env var.
115
112
 
116
- ## Available Tools
113
+ ## Available Tools (21 Total)
117
114
 
118
- ### Core Tools (All Modes)
115
+ ### Core Knowledge Tools (7)
119
116
 
120
117
  | Tool | Description |
121
118
  |------|-------------|
122
- | `search_knowledge` | Semantic search with relevance scores |
123
- | `answer_question` | Full RAG pipeline with auto-generation |
124
- | `get_article` | Retrieve complete article by ID |
125
- | `list_articles` | Browse articles with optional category filter |
126
- | `list_categories` | List all categories with article counts |
119
+ | `search_knowledge` | Semantic search across articles AND Q&A entries |
120
+ | `get_content` | Retrieve full content by ID (articles or Q&A) |
121
+ | `answer_question` | Full RAG pipeline with graph-augmented retrieval |
127
122
  | `log_unanswered` | Flag questions for knowledge gap analysis |
123
+ | `list_content` | Browse articles and Q&A with filtering |
124
+ | `list_categories` | List all categories with content counts |
128
125
  | `health_check` | Check system status and connectivity |
129
126
 
130
- ### Full Mode Tools
127
+ ### Knowledge Graph Tools (4)
131
128
 
132
129
  | Tool | Description |
133
130
  |------|-------------|
134
- | `add_article` | Create a new article with automatic embedding generation |
131
+ | `find_related` | Find related articles via shared entities |
132
+ | `get_entities` | Get named entities from an article |
133
+ | `search_entities` | Search entities by name or type |
134
+ | `get_knowledge_graph` | Get graph structure for visualization |
135
+
136
+ ### Episodic Memory Tools (5)
137
+
138
+ | Tool | Description |
139
+ |------|-------------|
140
+ | `start_conversation` | Begin tracking a conversation session |
141
+ | `add_message` | Add message with automatic fact extraction |
142
+ | `get_conversation` | Retrieve conversation with all messages |
143
+ | `list_conversations` | List recent conversations |
144
+ | `get_session_context` | Get context for follow-up questions |
145
+
146
+ ### Q&A Tools (1)
147
+
148
+ | Tool | Description |
149
+ |------|-------------|
150
+ | `add_question` | Add question for later expert answering |
151
+
152
+ ### Full Mode Tools (4)
153
+
154
+ | Tool | Description |
155
+ |------|-------------|
156
+ | `add_article` | Create a new article with automatic embedding |
135
157
  | `edit_article` | Update article content, metadata, or category |
136
- | `remove_article` | Soft-delete an article (preserves data for audit) |
137
- | `rate_answer` | Rate previous answers: -1 (unhelpful), 0 (neutral), 1 (helpful) |
158
+ | `remove_article` | Soft-delete an article |
159
+ | `rate_answer` | Rate previous answers: -1, 0, or 1 |
138
160
 
139
161
  ## MCP Resources
140
162
 
@@ -167,6 +189,18 @@ For full mode to work:
167
189
  1. The stdio proxy must have `--full` flag OR `MCP_MODE=full`
168
190
  2. The remote server must have `MCP_MODE=full` in its environment
169
191
 
192
+ ## Changelog
193
+
194
+ ### v2.2.0
195
+ - Added unified `get_content` and `list_content` tools (replacing `get_article`/`list_articles`)
196
+ - Added Knowledge Graph tools: `find_related`, `get_entities`, `search_entities`, `get_knowledge_graph`
197
+ - Added Episodic Memory tools: `start_conversation`, `add_message`, `get_conversation`, `list_conversations`, `get_session_context`
198
+ - Added Q&A tool: `add_question`
199
+ - Updated to MCP Protocol 2025-11-25
200
+
201
+ ### v2.1.1
202
+ - Initial public release with core knowledge tools
203
+
170
204
  ## License
171
205
 
172
206
  MIT
package/dist/index.d.ts CHANGED
@@ -11,8 +11,12 @@
11
11
  * npx @pragmatic-growth/memory-mcp --edit # Alias for --full
12
12
  *
13
13
  * Environment variables:
14
- * MCP_API_KEY - API key for authentication (required)
14
+ * MCP_API_KEY - Clerk API key for authentication (required)
15
15
  * MCP_SERVER_URL - Server URL (default: https://pg-memory-production.up.railway.app/api/mcp)
16
16
  * MCP_MODE - Mode: 'readonly' (default) or 'full' for edit capabilities
17
+ *
18
+ * Authentication:
19
+ * Uses Clerk API key authentication via X-API-Key header.
20
+ * Get your API key from the Clerk Dashboard or the app's API Docs page.
17
21
  */
18
22
  export {};
package/dist/index.js CHANGED
@@ -11,9 +11,13 @@
11
11
  * npx @pragmatic-growth/memory-mcp --edit # Alias for --full
12
12
  *
13
13
  * Environment variables:
14
- * MCP_API_KEY - API key for authentication (required)
14
+ * MCP_API_KEY - Clerk API key for authentication (required)
15
15
  * MCP_SERVER_URL - Server URL (default: https://pg-memory-production.up.railway.app/api/mcp)
16
16
  * MCP_MODE - Mode: 'readonly' (default) or 'full' for edit capabilities
17
+ *
18
+ * Authentication:
19
+ * Uses Clerk API key authentication via X-API-Key header.
20
+ * Get your API key from the Clerk Dashboard or the app's API Docs page.
17
21
  */
18
22
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
19
23
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -28,18 +32,20 @@ const SERVER_URL = process.env.MCP_SERVER_URL || 'https://pg-memory-production.u
28
32
  const API_KEY = process.env.MCP_API_KEY;
29
33
  if (!API_KEY) {
30
34
  console.error('Error: MCP_API_KEY environment variable is required');
31
- console.error('Set it in your Raycast MCP server configuration under "env"');
35
+ console.error('Get your Clerk API key from the Clerk Dashboard or app API Docs page');
36
+ console.error('Set it in your MCP client configuration under "env"');
32
37
  process.exit(1);
33
38
  }
34
39
  // HTTP session management
35
40
  let sessionId = null;
36
41
  /**
37
42
  * Make an HTTP request to the remote MCP server.
43
+ * Uses X-API-Key header for Clerk API key authentication.
38
44
  */
39
45
  async function callRemoteServer(method, params) {
40
46
  const headers = {
41
47
  'Content-Type': 'application/json',
42
- 'Authorization': `Bearer ${API_KEY}`,
48
+ 'X-API-Key': API_KEY, // Safe: already validated above
43
49
  };
44
50
  if (sessionId) {
45
51
  headers['Mcp-Session-Id'] = sessionId;
@@ -75,7 +81,7 @@ async function initializeRemoteSession() {
75
81
  capabilities: {},
76
82
  clientInfo: {
77
83
  name: 'pg-memory-stdio',
78
- version: '2.1.0',
84
+ version: '2.3.0',
79
85
  },
80
86
  });
81
87
  }
@@ -95,28 +101,30 @@ async function main() {
95
101
  // Create local MCP server
96
102
  const server = new McpServer({
97
103
  name: 'pg-memory',
98
- version: '2.1.0',
104
+ version: '2.3.0',
99
105
  description: `PG-Memory knowledge base (${modeLabel} mode)`,
100
106
  });
101
107
  // ============================================================
102
- // READ-ONLY TOOLS (available in all modes)
108
+ // CORE KNOWLEDGE TOOLS (available in all modes)
103
109
  // ============================================================
104
- // Tool 1: Search Knowledge (matches HTTP server's search_knowledge)
110
+ // Tool 1: Search Knowledge (Articles + Q&A)
105
111
  server.tool('search_knowledge', `Search the nonresident.tax knowledge base using semantic similarity.
106
112
 
107
- This knowledge base contains comprehensive information about:
108
- - Services: LLC formation, tax filing, registered agent, accounting
109
- - Pricing: Package costs, annual fees, consultation fees
113
+ **YOU MUST CALL THIS TOOL** before answering any domain-specific question.
114
+
115
+ Topics covered:
110
116
  - Tax compliance: Form 5472, Form 1120, ECI rules, penalties, deadlines
111
117
  - Company formation: Wyoming LLC, Delaware LLC, single-member LLC
112
118
  - Banking: Mercury, Wise, US bank account requirements
113
- - Process: Onboarding steps, document requirements, timelines
114
- - Dissolution: LLC closure, final filings, IRS compliance
119
+ - Services & pricing: LLC formation, tax filing, registered agent
115
120
 
116
- Returns relevant articles with relevance scores. Use get_article to retrieve full content.`, {
121
+ Returns relevant articles AND Q&A entries with relevance scores.
122
+ Use get_content with the ID to retrieve full content.`, {
117
123
  query: z.string().describe('The question or topic to search for'),
118
- limit: z.number().optional().describe('Maximum number of results to return (default: 5)'),
119
- threshold: z.number().optional().describe('Minimum similarity threshold 0-1 (default: 0.55)'),
124
+ limit: z.number().optional().describe('Maximum results to return (default: 5)'),
125
+ threshold: z.number().optional().describe('Minimum similarity 0-1 (default: 0.55)'),
126
+ include_articles: z.boolean().optional().describe('Include articles (default: true)'),
127
+ include_qa: z.boolean().optional().describe('Include Q&A entries (default: true)'),
120
128
  }, async (args) => {
121
129
  const result = await callRemoteServer('tools/call', {
122
130
  name: 'search_knowledge',
@@ -124,44 +132,38 @@ Returns relevant articles with relevance scores. Use get_article to retrieve ful
124
132
  });
125
133
  return result;
126
134
  });
127
- // Tool 2: Get Article
128
- server.tool('get_article', `Retrieve the complete content of a knowledge base article by ID.
135
+ // Tool 2: Get Content (Unified - Articles + Q&A)
136
+ server.tool('get_content', `Retrieve complete content from the knowledge base by ID.
129
137
 
130
- Use this tool when:
131
- - You have an article ID from search_knowledge or list_articles
132
- - You need the full article content (not just summary)
133
- - You want to verify specific details from search results
138
+ Automatically detects content type from ID prefix:
139
+ - 'art_*' Article (knowledge base article)
140
+ - 'qa_*' Q&A entry (question and answer)
134
141
 
135
- Returns: Full article with title, content, summary, category, tags, version, and last update date.`, {
136
- article_id: z.string().describe('The article ID (e.g., art_abc123)'),
142
+ Use summary_only=true first to check content size, then fetch with max_length if needed.`, {
143
+ content_id: z.string().describe('Content ID (e.g., art_abc123 or qa_xyz789)'),
144
+ max_length: z.number().optional().describe('Max content length in chars (truncates if exceeded)'),
145
+ summary_only: z.boolean().optional().describe('Return only summary/metadata (default: false)'),
137
146
  }, async (args) => {
138
147
  const result = await callRemoteServer('tools/call', {
139
- name: 'get_article',
148
+ name: 'get_content',
140
149
  arguments: args,
141
150
  });
142
151
  return result;
143
152
  });
144
153
  // Tool 3: Answer Question (Full RAG)
145
- server.tool('answer_question', `Answer a customer question using the knowledge base with full RAG.
146
-
147
- This tool:
148
- 1. Searches for relevant articles in the knowledge base
149
- 2. If found: Generates an answer synthesizing the retrieved content
150
- 3. If not found: Optionally generates a new article to fill the gap
154
+ server.tool('answer_question', `Answer a customer question using the knowledge base with full RAG pipeline.
151
155
 
152
- Use for questions about:
153
- - Services and pricing
154
- - Tax filing requirements and deadlines
155
- - LLC formation process
156
- - Bank account options
157
- - Document requirements
158
- - How onboarding works
159
- - Any nonresident.tax topic
156
+ How it works:
157
+ 1. Searches for relevant articles AND published Q&A
158
+ 2. Uses knowledge graph to find related content via entities
159
+ 3. Generates a synthesized answer from all sources
160
+ 4. Returns source references for transparency
160
161
 
161
- The answer includes source references for transparency.`, {
162
+ Use this when you need a complete answer with sources, not just search results.`, {
162
163
  question: z.string().describe('The customer question to answer'),
163
- context: z.string().optional().describe('Additional context about the question'),
164
- generate_if_not_found: z.boolean().optional().describe('Generate new article if no relevant content found (default: true)'),
164
+ context: z.string().optional().describe('Additional context'),
165
+ generate_if_not_found: z.boolean().optional().describe('Generate new article if not found (default: true)'),
166
+ use_graph: z.boolean().optional().describe('Enable graph-augmented retrieval (default: true)'),
165
167
  }, async (args) => {
166
168
  const result = await callRemoteServer('tools/call', {
167
169
  name: 'answer_question',
@@ -172,19 +174,10 @@ The answer includes source references for transparency.`, {
172
174
  // Tool 4: Log Unanswered
173
175
  server.tool('log_unanswered', `Flag a customer question as unanswered for knowledge gap analysis.
174
176
 
175
- WHEN TO USE:
176
- - After search_knowledge returns no relevant results
177
- - When existing articles don't fully answer the question
178
- - When a customer asks something completely new
179
- - To track recurring questions that need dedicated articles
180
-
181
- WHY IT MATTERS:
182
- - Flagged questions appear in the admin "Knowledge Gaps" section
183
- - Helps prioritize which new articles to create
184
- - Builds a list of topics customers actually need
185
- - Improves the knowledge base over time`, {
177
+ **CALL THIS** when search_knowledge returns no results or low confidence.
178
+ Flagged questions appear in admin "Knowledge Gaps" section.`, {
186
179
  query: z.string().describe('The question that could not be answered'),
187
- reason: z.string().optional().describe('Reason why the question could not be answered'),
180
+ reason: z.string().optional().describe('Reason why'),
188
181
  }, async (args) => {
189
182
  const result = await callRemoteServer('tools/call', {
190
183
  name: 'log_unanswered',
@@ -192,31 +185,24 @@ WHY IT MATTERS:
192
185
  });
193
186
  return result;
194
187
  });
195
- // Tool 5: List Articles
196
- server.tool('list_articles', `Browse articles in the knowledge base with optional filtering.
197
-
198
- Parameters:
199
- - limit: Number of articles (default 10, max 50)
200
- - category: Filter by category slug (use list_categories first)
188
+ // Tool 5: List Content (Unified - Articles + Q&A)
189
+ server.tool('list_content', `Browse content in the knowledge base with optional filtering.
201
190
 
202
- Returns article ID, title, and summary for each match.
203
- Use get_article with the ID to retrieve full content.
204
-
205
- WORKFLOW:
206
- 1. Call list_categories to discover available categories
207
- 2. Call list_articles with specific category for focused results
208
- 3. Call get_article for articles that look relevant`, {
209
- limit: z.number().optional().describe('Number of articles to return (max 50)'),
191
+ Supports: articles, qa, or all (default).
192
+ Use list_categories first to discover available categories.`, {
193
+ type: z.enum(['articles', 'qa', 'all']).optional().describe('Content type (default: all)'),
194
+ limit: z.number().optional().describe('Number of items (default: 10, max: 50)'),
210
195
  category: z.string().optional().describe('Filter by category slug'),
196
+ status: z.string().optional().describe('Filter Q&A by status: pending, draft, published'),
211
197
  }, async (args) => {
212
198
  const result = await callRemoteServer('tools/call', {
213
- name: 'list_articles',
199
+ name: 'list_content',
214
200
  arguments: args,
215
201
  });
216
202
  return result;
217
203
  });
218
204
  // Tool 6: Health Check
219
- server.tool('health_check', 'Check the health status of the memory system.', {}, async () => {
205
+ server.tool('health_check', 'Check the health status of the memory system including article/Q&A counts.', {}, async () => {
220
206
  const result = await callRemoteServer('tools/call', {
221
207
  name: 'health_check',
222
208
  arguments: {},
@@ -224,22 +210,8 @@ WORKFLOW:
224
210
  return result;
225
211
  });
226
212
  // Tool 7: List Categories
227
- server.tool('list_categories', `List all categories in the knowledge base with article counts.
228
-
229
- CRITICAL FOR EFFICIENT SEARCH:
230
- Categories help you quickly narrow down to relevant content.
231
-
232
- Returns for each category:
233
- - slug: Category identifier for filtering
234
- - name: Human-readable name
235
- - count: Number of articles
236
- - description: What the category covers
237
-
238
- RECOMMENDED WORKFLOW:
239
- 1. Call list_categories to see what's available
240
- 2. Call list_articles with category slug
241
- 3. Call get_article for full content
242
- 4. Or use search_knowledge for semantic search across all`, {}, async () => {
213
+ server.tool('list_categories', `List all categories in the knowledge base with content counts.
214
+ Use category slugs with list_content for filtered browsing.`, {}, async () => {
243
215
  const result = await callRemoteServer('tools/call', {
244
216
  name: 'list_categories',
245
217
  arguments: {},
@@ -247,17 +219,152 @@ RECOMMENDED WORKFLOW:
247
219
  return result;
248
220
  });
249
221
  // ============================================================
250
- // FULL MODE TOOLS (only available with --full flag or MCP_MODE=full)
222
+ // KNOWLEDGE GRAPH TOOLS (available in all modes)
223
+ // ============================================================
224
+ // Tool 8: Find Related Articles
225
+ server.tool('find_related', 'Find articles related through shared entities or explicit relationships. Uses graph traversal.', {
226
+ article_id: z.string().describe('Article ID to find relations for'),
227
+ max_depth: z.number().optional().describe('Relationship hops 1-5 (default: 2)'),
228
+ min_strength: z.number().optional().describe('Min strength 0-1 (default: 0.3)'),
229
+ limit: z.number().optional().describe('Max articles (default: 10)'),
230
+ }, async (args) => {
231
+ const result = await callRemoteServer('tools/call', {
232
+ name: 'find_related',
233
+ arguments: args,
234
+ });
235
+ return result;
236
+ });
237
+ // Tool 9: Get Entities
238
+ server.tool('get_entities', 'Get named entities from an article (people, orgs, forms, states, etc.).', {
239
+ article_id: z.string().describe('Article ID to get entities for'),
240
+ }, async (args) => {
241
+ const result = await callRemoteServer('tools/call', {
242
+ name: 'get_entities',
243
+ arguments: args,
244
+ });
245
+ return result;
246
+ });
247
+ // Tool 10: Search Entities
248
+ server.tool('search_entities', 'Search for entities by name or type across the knowledge base.', {
249
+ query: z.string().optional().describe('Search query for entity name'),
250
+ type: z.string().optional().describe('Filter: PERSON, ORGANIZATION, FORM, STATE, COUNTRY, DATE, AMOUNT, CONCEPT, REGULATION, SERVICE, DOCUMENT'),
251
+ limit: z.number().optional().describe('Max entities (default: 20)'),
252
+ }, async (args) => {
253
+ const result = await callRemoteServer('tools/call', {
254
+ name: 'search_entities',
255
+ arguments: args,
256
+ });
257
+ return result;
258
+ });
259
+ // Tool 11: Get Knowledge Graph
260
+ server.tool('get_knowledge_graph', 'Get the knowledge graph structure for visualization. Returns nodes and edges.', {
261
+ center_article_id: z.string().optional().describe('Center on this article'),
262
+ depth: z.number().optional().describe('Traversal depth 1-5 (default: 2)'),
263
+ include_entities: z.boolean().optional().describe('Include entity nodes (default: true)'),
264
+ include_metadata: z.boolean().optional().describe('Include full metadata (default: false)'),
265
+ limit: z.number().optional().describe('Max nodes (default: 20, max: 100)'),
266
+ }, async (args) => {
267
+ const result = await callRemoteServer('tools/call', {
268
+ name: 'get_knowledge_graph',
269
+ arguments: args,
270
+ });
271
+ return result;
272
+ });
273
+ // ============================================================
274
+ // EPISODIC MEMORY TOOLS (available in all modes)
275
+ // ============================================================
276
+ // Tool 12: Start Conversation
277
+ server.tool('start_conversation', `Start a new conversation for tracking episodic memory.
278
+ Call at the beginning of each session to enable context continuity.`, {
279
+ session_id: z.string().describe('Session ID (e.g., MCP session ID)'),
280
+ title: z.string().optional().describe('Conversation title'),
281
+ user_id: z.string().optional().describe('User ID for multi-user tracking'),
282
+ }, async (args) => {
283
+ const result = await callRemoteServer('tools/call', {
284
+ name: 'start_conversation',
285
+ arguments: args,
286
+ });
287
+ return result;
288
+ });
289
+ // Tool 13: Add Message
290
+ server.tool('add_message', `Add a message to an existing conversation.
291
+ Call after each user message and assistant response.
292
+ User facts are automatically extracted.`, {
293
+ conversation_id: z.string().describe('Conversation ID from start_conversation'),
294
+ role: z.enum(['user', 'assistant', 'system']).describe('Message role'),
295
+ content: z.string().describe('Message content'),
296
+ referenced_articles: z.string().optional().describe('Comma-separated article IDs referenced'),
297
+ }, async (args) => {
298
+ const result = await callRemoteServer('tools/call', {
299
+ name: 'add_message',
300
+ arguments: args,
301
+ });
302
+ return result;
303
+ });
304
+ // Tool 14: Get Conversation
305
+ server.tool('get_conversation', 'Retrieve a conversation with all its messages by ID.', {
306
+ conversation_id: z.string().describe('Conversation ID'),
307
+ }, async (args) => {
308
+ const result = await callRemoteServer('tools/call', {
309
+ name: 'get_conversation',
310
+ arguments: args,
311
+ });
312
+ return result;
313
+ });
314
+ // Tool 15: List Conversations
315
+ server.tool('list_conversations', 'List conversations for a session or user. Returns recent first.', {
316
+ session_id: z.string().optional().describe('Filter by session ID'),
317
+ user_id: z.string().optional().describe('Filter by user ID'),
318
+ limit: z.number().optional().describe('Max conversations (default: 20)'),
319
+ }, async (args) => {
320
+ const result = await callRemoteServer('tools/call', {
321
+ name: 'list_conversations',
322
+ arguments: args,
323
+ });
324
+ return result;
325
+ });
326
+ // Tool 16: Get Session Context
327
+ server.tool('get_session_context', `Get recent conversation context for a session.
328
+ Call when user refers to prior conversation or asks follow-up questions.`, {
329
+ session_id: z.string().describe('Session ID to get context for'),
330
+ message_limit: z.number().optional().describe('Max messages (default: 10)'),
331
+ }, async (args) => {
332
+ const result = await callRemoteServer('tools/call', {
333
+ name: 'get_session_context',
334
+ arguments: args,
335
+ });
336
+ return result;
337
+ });
338
+ // ============================================================
339
+ // Q&A TOOLS (available in all modes)
340
+ // ============================================================
341
+ // Tool 17: Add Question
342
+ server.tool('add_question', `Add a new question to the Q&A store for later answering.
343
+ Use to capture questions from external channels.`, {
344
+ question: z.string().describe('The question text'),
345
+ category: z.string().optional().describe('Category slug'),
346
+ tags: z.string().optional().describe('Comma-separated tags'),
347
+ priority: z.string().optional().describe('Priority: low, medium, high, critical'),
348
+ source: z.string().optional().describe('Source: mcp, email, manual, import'),
349
+ }, async (args) => {
350
+ const result = await callRemoteServer('tools/call', {
351
+ name: 'add_question',
352
+ arguments: args,
353
+ });
354
+ return result;
355
+ });
356
+ // ============================================================
357
+ // FULL MODE TOOLS (only with --full flag or MCP_MODE=full)
251
358
  // ============================================================
252
359
  if (isFullMode) {
253
- // Tool 8: Add Article
254
- server.tool('add_article', 'Create a new article in the knowledge base. Automatically generates embedding for semantic search.', {
360
+ // Tool 18: Add Article
361
+ server.tool('add_article', 'Create a new article in the knowledge base. Generates embedding automatically.', {
255
362
  title: z.string().describe('Article title'),
256
- content: z.string().describe('Article content in markdown format'),
257
- summary: z.string().optional().describe('Brief summary (auto-generated if not provided)'),
258
- category: z.string().optional().describe('Category (e.g., federal-tax, state-compliance)'),
363
+ content: z.string().describe('Article content in markdown'),
364
+ summary: z.string().optional().describe('Brief summary (auto-generated if omitted)'),
365
+ category: z.string().optional().describe('Category slug'),
259
366
  tags: z.string().optional().describe('Comma-separated tags'),
260
- source: z.string().optional().describe('Source of the article (e.g., manual, irs.gov)'),
367
+ source: z.string().optional().describe('Source (e.g., manual, irs.gov)'),
261
368
  }, async (args) => {
262
369
  const result = await callRemoteServer('tools/call', {
263
370
  name: 'add_article',
@@ -265,7 +372,7 @@ RECOMMENDED WORKFLOW:
265
372
  });
266
373
  return result;
267
374
  });
268
- // Tool 9: Edit Article
375
+ // Tool 19: Edit Article
269
376
  server.tool('edit_article', 'Update an existing article. Re-generates embedding if content changes.', {
270
377
  article_id: z.string().describe('Article ID to update'),
271
378
  title: z.string().optional().describe('New title'),
@@ -280,10 +387,10 @@ RECOMMENDED WORKFLOW:
280
387
  });
281
388
  return result;
282
389
  });
283
- // Tool 10: Remove Article
284
- server.tool('remove_article', 'Soft-delete an article from the knowledge base. Article is marked as deleted but not permanently removed.', {
390
+ // Tool 20: Remove Article
391
+ server.tool('remove_article', 'Soft-delete an article. Marked as deleted but not permanently removed.', {
285
392
  article_id: z.string().describe('Article ID to remove'),
286
- reason: z.string().optional().describe('Reason for deletion (for audit log)'),
393
+ reason: z.string().optional().describe('Reason for deletion'),
287
394
  }, async (args) => {
288
395
  const result = await callRemoteServer('tools/call', {
289
396
  name: 'remove_article',
@@ -291,11 +398,11 @@ RECOMMENDED WORKFLOW:
291
398
  });
292
399
  return result;
293
400
  });
294
- // Tool 11: Rate Answer
295
- server.tool('rate_answer', 'Rate a previous answer as helpful, unhelpful, or neutral. Helps improve the knowledge base.', {
296
- query_id: z.string().describe('Query log ID from a previous answer_question response'),
401
+ // Tool 21: Rate Answer
402
+ server.tool('rate_answer', 'Rate a previous answer as helpful, unhelpful, or neutral.', {
403
+ query_id: z.string().describe('Query log ID from answer_question'),
297
404
  rating: z.number().describe('Rating: -1 (unhelpful), 0 (neutral), 1 (helpful)'),
298
- feedback: z.string().optional().describe('Optional text feedback'),
405
+ feedback: z.string().optional().describe('Optional feedback text'),
299
406
  }, async (args) => {
300
407
  const result = await callRemoteServer('tools/call', {
301
408
  name: 'rate_answer',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pragmatic-growth/memory-mcp",
3
- "version": "2.1.1",
4
- "description": "Stdio proxy for PG-Memory - connects stdio-based MCP clients (Raycast, local tools) to your PG-Memory HTTP server. PG-Memory supports both HTTP transport (Claude Code, mcp-remote) and stdio transport (this package).",
3
+ "version": "2.3.0",
4
+ "description": "Stdio proxy for PG-Memory - connects stdio-based MCP clients (Claude Desktop, Raycast) to your PG-Memory HTTP server using Clerk API key authentication. Supports both read-only and full edit modes.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "memory-mcp": "./dist/index.js"
package/src/index.ts CHANGED
@@ -11,9 +11,13 @@
11
11
  * npx @pragmatic-growth/memory-mcp --edit # Alias for --full
12
12
  *
13
13
  * Environment variables:
14
- * MCP_API_KEY - API key for authentication (required)
14
+ * MCP_API_KEY - Clerk API key for authentication (required)
15
15
  * MCP_SERVER_URL - Server URL (default: https://pg-memory-production.up.railway.app/api/mcp)
16
16
  * MCP_MODE - Mode: 'readonly' (default) or 'full' for edit capabilities
17
+ *
18
+ * Authentication:
19
+ * Uses Clerk API key authentication via X-API-Key header.
20
+ * Get your API key from the Clerk Dashboard or the app's API Docs page.
17
21
  */
18
22
 
19
23
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
@@ -32,7 +36,8 @@ const API_KEY = process.env.MCP_API_KEY;
32
36
 
33
37
  if (!API_KEY) {
34
38
  console.error('Error: MCP_API_KEY environment variable is required');
35
- console.error('Set it in your Raycast MCP server configuration under "env"');
39
+ console.error('Get your Clerk API key from the Clerk Dashboard or app API Docs page');
40
+ console.error('Set it in your MCP client configuration under "env"');
36
41
  process.exit(1);
37
42
  }
38
43
 
@@ -49,11 +54,12 @@ interface JsonRpcResponse {
49
54
 
50
55
  /**
51
56
  * Make an HTTP request to the remote MCP server.
57
+ * Uses X-API-Key header for Clerk API key authentication.
52
58
  */
53
59
  async function callRemoteServer(method: string, params?: Record<string, unknown>): Promise<unknown> {
54
60
  const headers: Record<string, string> = {
55
61
  'Content-Type': 'application/json',
56
- 'Authorization': `Bearer ${API_KEY}`,
62
+ 'X-API-Key': API_KEY!, // Safe: already validated above
57
63
  };
58
64
 
59
65
  if (sessionId) {
@@ -97,7 +103,7 @@ async function initializeRemoteSession(): Promise<void> {
97
103
  capabilities: {},
98
104
  clientInfo: {
99
105
  name: 'pg-memory-stdio',
100
- version: '2.1.0',
106
+ version: '2.3.0',
101
107
  },
102
108
  });
103
109
  }
@@ -119,33 +125,35 @@ async function main(): Promise<void> {
119
125
  // Create local MCP server
120
126
  const server = new McpServer({
121
127
  name: 'pg-memory',
122
- version: '2.1.0',
128
+ version: '2.3.0',
123
129
  description: `PG-Memory knowledge base (${modeLabel} mode)`,
124
130
  });
125
131
 
126
132
  // ============================================================
127
- // READ-ONLY TOOLS (available in all modes)
133
+ // CORE KNOWLEDGE TOOLS (available in all modes)
128
134
  // ============================================================
129
135
 
130
- // Tool 1: Search Knowledge (matches HTTP server's search_knowledge)
136
+ // Tool 1: Search Knowledge (Articles + Q&A)
131
137
  server.tool(
132
138
  'search_knowledge',
133
139
  `Search the nonresident.tax knowledge base using semantic similarity.
134
140
 
135
- This knowledge base contains comprehensive information about:
136
- - Services: LLC formation, tax filing, registered agent, accounting
137
- - Pricing: Package costs, annual fees, consultation fees
141
+ **YOU MUST CALL THIS TOOL** before answering any domain-specific question.
142
+
143
+ Topics covered:
138
144
  - Tax compliance: Form 5472, Form 1120, ECI rules, penalties, deadlines
139
145
  - Company formation: Wyoming LLC, Delaware LLC, single-member LLC
140
146
  - Banking: Mercury, Wise, US bank account requirements
141
- - Process: Onboarding steps, document requirements, timelines
142
- - Dissolution: LLC closure, final filings, IRS compliance
147
+ - Services & pricing: LLC formation, tax filing, registered agent
143
148
 
144
- Returns relevant articles with relevance scores. Use get_article to retrieve full content.`,
149
+ Returns relevant articles AND Q&A entries with relevance scores.
150
+ Use get_content with the ID to retrieve full content.`,
145
151
  {
146
152
  query: z.string().describe('The question or topic to search for'),
147
- limit: z.number().optional().describe('Maximum number of results to return (default: 5)'),
148
- threshold: z.number().optional().describe('Minimum similarity threshold 0-1 (default: 0.55)'),
153
+ limit: z.number().optional().describe('Maximum results to return (default: 5)'),
154
+ threshold: z.number().optional().describe('Minimum similarity 0-1 (default: 0.55)'),
155
+ include_articles: z.boolean().optional().describe('Include articles (default: true)'),
156
+ include_qa: z.boolean().optional().describe('Include Q&A entries (default: true)'),
149
157
  },
150
158
  async (args) => {
151
159
  const result = await callRemoteServer('tools/call', {
@@ -156,23 +164,24 @@ Returns relevant articles with relevance scores. Use get_article to retrieve ful
156
164
  }
157
165
  );
158
166
 
159
- // Tool 2: Get Article
167
+ // Tool 2: Get Content (Unified - Articles + Q&A)
160
168
  server.tool(
161
- 'get_article',
162
- `Retrieve the complete content of a knowledge base article by ID.
169
+ 'get_content',
170
+ `Retrieve complete content from the knowledge base by ID.
163
171
 
164
- Use this tool when:
165
- - You have an article ID from search_knowledge or list_articles
166
- - You need the full article content (not just summary)
167
- - You want to verify specific details from search results
172
+ Automatically detects content type from ID prefix:
173
+ - 'art_*' Article (knowledge base article)
174
+ - 'qa_*' Q&A entry (question and answer)
168
175
 
169
- Returns: Full article with title, content, summary, category, tags, version, and last update date.`,
176
+ Use summary_only=true first to check content size, then fetch with max_length if needed.`,
170
177
  {
171
- article_id: z.string().describe('The article ID (e.g., art_abc123)'),
178
+ content_id: z.string().describe('Content ID (e.g., art_abc123 or qa_xyz789)'),
179
+ max_length: z.number().optional().describe('Max content length in chars (truncates if exceeded)'),
180
+ summary_only: z.boolean().optional().describe('Return only summary/metadata (default: false)'),
172
181
  },
173
182
  async (args) => {
174
183
  const result = await callRemoteServer('tools/call', {
175
- name: 'get_article',
184
+ name: 'get_content',
176
185
  arguments: args,
177
186
  }) as { content: Array<{ type: 'text'; text: string }> };
178
187
  return result;
@@ -182,27 +191,20 @@ Returns: Full article with title, content, summary, category, tags, version, and
182
191
  // Tool 3: Answer Question (Full RAG)
183
192
  server.tool(
184
193
  'answer_question',
185
- `Answer a customer question using the knowledge base with full RAG.
186
-
187
- This tool:
188
- 1. Searches for relevant articles in the knowledge base
189
- 2. If found: Generates an answer synthesizing the retrieved content
190
- 3. If not found: Optionally generates a new article to fill the gap
191
-
192
- Use for questions about:
193
- - Services and pricing
194
- - Tax filing requirements and deadlines
195
- - LLC formation process
196
- - Bank account options
197
- - Document requirements
198
- - How onboarding works
199
- - Any nonresident.tax topic
200
-
201
- The answer includes source references for transparency.`,
194
+ `Answer a customer question using the knowledge base with full RAG pipeline.
195
+
196
+ How it works:
197
+ 1. Searches for relevant articles AND published Q&A
198
+ 2. Uses knowledge graph to find related content via entities
199
+ 3. Generates a synthesized answer from all sources
200
+ 4. Returns source references for transparency
201
+
202
+ Use this when you need a complete answer with sources, not just search results.`,
202
203
  {
203
204
  question: z.string().describe('The customer question to answer'),
204
- context: z.string().optional().describe('Additional context about the question'),
205
- generate_if_not_found: z.boolean().optional().describe('Generate new article if no relevant content found (default: true)'),
205
+ context: z.string().optional().describe('Additional context'),
206
+ generate_if_not_found: z.boolean().optional().describe('Generate new article if not found (default: true)'),
207
+ use_graph: z.boolean().optional().describe('Enable graph-augmented retrieval (default: true)'),
206
208
  },
207
209
  async (args) => {
208
210
  const result = await callRemoteServer('tools/call', {
@@ -218,20 +220,11 @@ The answer includes source references for transparency.`,
218
220
  'log_unanswered',
219
221
  `Flag a customer question as unanswered for knowledge gap analysis.
220
222
 
221
- WHEN TO USE:
222
- - After search_knowledge returns no relevant results
223
- - When existing articles don't fully answer the question
224
- - When a customer asks something completely new
225
- - To track recurring questions that need dedicated articles
226
-
227
- WHY IT MATTERS:
228
- - Flagged questions appear in the admin "Knowledge Gaps" section
229
- - Helps prioritize which new articles to create
230
- - Builds a list of topics customers actually need
231
- - Improves the knowledge base over time`,
223
+ **CALL THIS** when search_knowledge returns no results or low confidence.
224
+ Flagged questions appear in admin "Knowledge Gaps" section.`,
232
225
  {
233
226
  query: z.string().describe('The question that could not be answered'),
234
- reason: z.string().optional().describe('Reason why the question could not be answered'),
227
+ reason: z.string().optional().describe('Reason why'),
235
228
  },
236
229
  async (args) => {
237
230
  const result = await callRemoteServer('tools/call', {
@@ -242,29 +235,22 @@ WHY IT MATTERS:
242
235
  }
243
236
  );
244
237
 
245
- // Tool 5: List Articles
238
+ // Tool 5: List Content (Unified - Articles + Q&A)
246
239
  server.tool(
247
- 'list_articles',
248
- `Browse articles in the knowledge base with optional filtering.
240
+ 'list_content',
241
+ `Browse content in the knowledge base with optional filtering.
249
242
 
250
- Parameters:
251
- - limit: Number of articles (default 10, max 50)
252
- - category: Filter by category slug (use list_categories first)
253
-
254
- Returns article ID, title, and summary for each match.
255
- Use get_article with the ID to retrieve full content.
256
-
257
- WORKFLOW:
258
- 1. Call list_categories to discover available categories
259
- 2. Call list_articles with specific category for focused results
260
- 3. Call get_article for articles that look relevant`,
243
+ Supports: articles, qa, or all (default).
244
+ Use list_categories first to discover available categories.`,
261
245
  {
262
- limit: z.number().optional().describe('Number of articles to return (max 50)'),
246
+ type: z.enum(['articles', 'qa', 'all']).optional().describe('Content type (default: all)'),
247
+ limit: z.number().optional().describe('Number of items (default: 10, max: 50)'),
263
248
  category: z.string().optional().describe('Filter by category slug'),
249
+ status: z.string().optional().describe('Filter Q&A by status: pending, draft, published'),
264
250
  },
265
251
  async (args) => {
266
252
  const result = await callRemoteServer('tools/call', {
267
- name: 'list_articles',
253
+ name: 'list_content',
268
254
  arguments: args,
269
255
  }) as { content: Array<{ type: 'text'; text: string }> };
270
256
  return result;
@@ -274,7 +260,7 @@ WORKFLOW:
274
260
  // Tool 6: Health Check
275
261
  server.tool(
276
262
  'health_check',
277
- 'Check the health status of the memory system.',
263
+ 'Check the health status of the memory system including article/Q&A counts.',
278
264
  {},
279
265
  async () => {
280
266
  const result = await callRemoteServer('tools/call', {
@@ -288,22 +274,8 @@ WORKFLOW:
288
274
  // Tool 7: List Categories
289
275
  server.tool(
290
276
  'list_categories',
291
- `List all categories in the knowledge base with article counts.
292
-
293
- CRITICAL FOR EFFICIENT SEARCH:
294
- Categories help you quickly narrow down to relevant content.
295
-
296
- Returns for each category:
297
- - slug: Category identifier for filtering
298
- - name: Human-readable name
299
- - count: Number of articles
300
- - description: What the category covers
301
-
302
- RECOMMENDED WORKFLOW:
303
- 1. Call list_categories to see what's available
304
- 2. Call list_articles with category slug
305
- 3. Call get_article for full content
306
- 4. Or use search_knowledge for semantic search across all`,
277
+ `List all categories in the knowledge base with content counts.
278
+ Use category slugs with list_content for filtered browsing.`,
307
279
  {},
308
280
  async () => {
309
281
  const result = await callRemoteServer('tools/call', {
@@ -315,21 +287,219 @@ RECOMMENDED WORKFLOW:
315
287
  );
316
288
 
317
289
  // ============================================================
318
- // FULL MODE TOOLS (only available with --full flag or MCP_MODE=full)
290
+ // KNOWLEDGE GRAPH TOOLS (available in all modes)
291
+ // ============================================================
292
+
293
+ // Tool 8: Find Related Articles
294
+ server.tool(
295
+ 'find_related',
296
+ 'Find articles related through shared entities or explicit relationships. Uses graph traversal.',
297
+ {
298
+ article_id: z.string().describe('Article ID to find relations for'),
299
+ max_depth: z.number().optional().describe('Relationship hops 1-5 (default: 2)'),
300
+ min_strength: z.number().optional().describe('Min strength 0-1 (default: 0.3)'),
301
+ limit: z.number().optional().describe('Max articles (default: 10)'),
302
+ },
303
+ async (args) => {
304
+ const result = await callRemoteServer('tools/call', {
305
+ name: 'find_related',
306
+ arguments: args,
307
+ }) as { content: Array<{ type: 'text'; text: string }> };
308
+ return result;
309
+ }
310
+ );
311
+
312
+ // Tool 9: Get Entities
313
+ server.tool(
314
+ 'get_entities',
315
+ 'Get named entities from an article (people, orgs, forms, states, etc.).',
316
+ {
317
+ article_id: z.string().describe('Article ID to get entities for'),
318
+ },
319
+ async (args) => {
320
+ const result = await callRemoteServer('tools/call', {
321
+ name: 'get_entities',
322
+ arguments: args,
323
+ }) as { content: Array<{ type: 'text'; text: string }> };
324
+ return result;
325
+ }
326
+ );
327
+
328
+ // Tool 10: Search Entities
329
+ server.tool(
330
+ 'search_entities',
331
+ 'Search for entities by name or type across the knowledge base.',
332
+ {
333
+ query: z.string().optional().describe('Search query for entity name'),
334
+ type: z.string().optional().describe('Filter: PERSON, ORGANIZATION, FORM, STATE, COUNTRY, DATE, AMOUNT, CONCEPT, REGULATION, SERVICE, DOCUMENT'),
335
+ limit: z.number().optional().describe('Max entities (default: 20)'),
336
+ },
337
+ async (args) => {
338
+ const result = await callRemoteServer('tools/call', {
339
+ name: 'search_entities',
340
+ arguments: args,
341
+ }) as { content: Array<{ type: 'text'; text: string }> };
342
+ return result;
343
+ }
344
+ );
345
+
346
+ // Tool 11: Get Knowledge Graph
347
+ server.tool(
348
+ 'get_knowledge_graph',
349
+ 'Get the knowledge graph structure for visualization. Returns nodes and edges.',
350
+ {
351
+ center_article_id: z.string().optional().describe('Center on this article'),
352
+ depth: z.number().optional().describe('Traversal depth 1-5 (default: 2)'),
353
+ include_entities: z.boolean().optional().describe('Include entity nodes (default: true)'),
354
+ include_metadata: z.boolean().optional().describe('Include full metadata (default: false)'),
355
+ limit: z.number().optional().describe('Max nodes (default: 20, max: 100)'),
356
+ },
357
+ async (args) => {
358
+ const result = await callRemoteServer('tools/call', {
359
+ name: 'get_knowledge_graph',
360
+ arguments: args,
361
+ }) as { content: Array<{ type: 'text'; text: string }> };
362
+ return result;
363
+ }
364
+ );
365
+
366
+ // ============================================================
367
+ // EPISODIC MEMORY TOOLS (available in all modes)
368
+ // ============================================================
369
+
370
+ // Tool 12: Start Conversation
371
+ server.tool(
372
+ 'start_conversation',
373
+ `Start a new conversation for tracking episodic memory.
374
+ Call at the beginning of each session to enable context continuity.`,
375
+ {
376
+ session_id: z.string().describe('Session ID (e.g., MCP session ID)'),
377
+ title: z.string().optional().describe('Conversation title'),
378
+ user_id: z.string().optional().describe('User ID for multi-user tracking'),
379
+ },
380
+ async (args) => {
381
+ const result = await callRemoteServer('tools/call', {
382
+ name: 'start_conversation',
383
+ arguments: args,
384
+ }) as { content: Array<{ type: 'text'; text: string }> };
385
+ return result;
386
+ }
387
+ );
388
+
389
+ // Tool 13: Add Message
390
+ server.tool(
391
+ 'add_message',
392
+ `Add a message to an existing conversation.
393
+ Call after each user message and assistant response.
394
+ User facts are automatically extracted.`,
395
+ {
396
+ conversation_id: z.string().describe('Conversation ID from start_conversation'),
397
+ role: z.enum(['user', 'assistant', 'system']).describe('Message role'),
398
+ content: z.string().describe('Message content'),
399
+ referenced_articles: z.string().optional().describe('Comma-separated article IDs referenced'),
400
+ },
401
+ async (args) => {
402
+ const result = await callRemoteServer('tools/call', {
403
+ name: 'add_message',
404
+ arguments: args,
405
+ }) as { content: Array<{ type: 'text'; text: string }> };
406
+ return result;
407
+ }
408
+ );
409
+
410
+ // Tool 14: Get Conversation
411
+ server.tool(
412
+ 'get_conversation',
413
+ 'Retrieve a conversation with all its messages by ID.',
414
+ {
415
+ conversation_id: z.string().describe('Conversation ID'),
416
+ },
417
+ async (args) => {
418
+ const result = await callRemoteServer('tools/call', {
419
+ name: 'get_conversation',
420
+ arguments: args,
421
+ }) as { content: Array<{ type: 'text'; text: string }> };
422
+ return result;
423
+ }
424
+ );
425
+
426
+ // Tool 15: List Conversations
427
+ server.tool(
428
+ 'list_conversations',
429
+ 'List conversations for a session or user. Returns recent first.',
430
+ {
431
+ session_id: z.string().optional().describe('Filter by session ID'),
432
+ user_id: z.string().optional().describe('Filter by user ID'),
433
+ limit: z.number().optional().describe('Max conversations (default: 20)'),
434
+ },
435
+ async (args) => {
436
+ const result = await callRemoteServer('tools/call', {
437
+ name: 'list_conversations',
438
+ arguments: args,
439
+ }) as { content: Array<{ type: 'text'; text: string }> };
440
+ return result;
441
+ }
442
+ );
443
+
444
+ // Tool 16: Get Session Context
445
+ server.tool(
446
+ 'get_session_context',
447
+ `Get recent conversation context for a session.
448
+ Call when user refers to prior conversation or asks follow-up questions.`,
449
+ {
450
+ session_id: z.string().describe('Session ID to get context for'),
451
+ message_limit: z.number().optional().describe('Max messages (default: 10)'),
452
+ },
453
+ async (args) => {
454
+ const result = await callRemoteServer('tools/call', {
455
+ name: 'get_session_context',
456
+ arguments: args,
457
+ }) as { content: Array<{ type: 'text'; text: string }> };
458
+ return result;
459
+ }
460
+ );
461
+
462
+ // ============================================================
463
+ // Q&A TOOLS (available in all modes)
464
+ // ============================================================
465
+
466
+ // Tool 17: Add Question
467
+ server.tool(
468
+ 'add_question',
469
+ `Add a new question to the Q&A store for later answering.
470
+ Use to capture questions from external channels.`,
471
+ {
472
+ question: z.string().describe('The question text'),
473
+ category: z.string().optional().describe('Category slug'),
474
+ tags: z.string().optional().describe('Comma-separated tags'),
475
+ priority: z.string().optional().describe('Priority: low, medium, high, critical'),
476
+ source: z.string().optional().describe('Source: mcp, email, manual, import'),
477
+ },
478
+ async (args) => {
479
+ const result = await callRemoteServer('tools/call', {
480
+ name: 'add_question',
481
+ arguments: args,
482
+ }) as { content: Array<{ type: 'text'; text: string }> };
483
+ return result;
484
+ }
485
+ );
486
+
487
+ // ============================================================
488
+ // FULL MODE TOOLS (only with --full flag or MCP_MODE=full)
319
489
  // ============================================================
320
490
 
321
491
  if (isFullMode) {
322
- // Tool 8: Add Article
492
+ // Tool 18: Add Article
323
493
  server.tool(
324
494
  'add_article',
325
- 'Create a new article in the knowledge base. Automatically generates embedding for semantic search.',
495
+ 'Create a new article in the knowledge base. Generates embedding automatically.',
326
496
  {
327
497
  title: z.string().describe('Article title'),
328
- content: z.string().describe('Article content in markdown format'),
329
- summary: z.string().optional().describe('Brief summary (auto-generated if not provided)'),
330
- category: z.string().optional().describe('Category (e.g., federal-tax, state-compliance)'),
498
+ content: z.string().describe('Article content in markdown'),
499
+ summary: z.string().optional().describe('Brief summary (auto-generated if omitted)'),
500
+ category: z.string().optional().describe('Category slug'),
331
501
  tags: z.string().optional().describe('Comma-separated tags'),
332
- source: z.string().optional().describe('Source of the article (e.g., manual, irs.gov)'),
502
+ source: z.string().optional().describe('Source (e.g., manual, irs.gov)'),
333
503
  },
334
504
  async (args) => {
335
505
  const result = await callRemoteServer('tools/call', {
@@ -340,7 +510,7 @@ RECOMMENDED WORKFLOW:
340
510
  }
341
511
  );
342
512
 
343
- // Tool 9: Edit Article
513
+ // Tool 19: Edit Article
344
514
  server.tool(
345
515
  'edit_article',
346
516
  'Update an existing article. Re-generates embedding if content changes.',
@@ -361,13 +531,13 @@ RECOMMENDED WORKFLOW:
361
531
  }
362
532
  );
363
533
 
364
- // Tool 10: Remove Article
534
+ // Tool 20: Remove Article
365
535
  server.tool(
366
536
  'remove_article',
367
- 'Soft-delete an article from the knowledge base. Article is marked as deleted but not permanently removed.',
537
+ 'Soft-delete an article. Marked as deleted but not permanently removed.',
368
538
  {
369
539
  article_id: z.string().describe('Article ID to remove'),
370
- reason: z.string().optional().describe('Reason for deletion (for audit log)'),
540
+ reason: z.string().optional().describe('Reason for deletion'),
371
541
  },
372
542
  async (args) => {
373
543
  const result = await callRemoteServer('tools/call', {
@@ -378,14 +548,14 @@ RECOMMENDED WORKFLOW:
378
548
  }
379
549
  );
380
550
 
381
- // Tool 11: Rate Answer
551
+ // Tool 21: Rate Answer
382
552
  server.tool(
383
553
  'rate_answer',
384
- 'Rate a previous answer as helpful, unhelpful, or neutral. Helps improve the knowledge base.',
554
+ 'Rate a previous answer as helpful, unhelpful, or neutral.',
385
555
  {
386
- query_id: z.string().describe('Query log ID from a previous answer_question response'),
556
+ query_id: z.string().describe('Query log ID from answer_question'),
387
557
  rating: z.number().describe('Rating: -1 (unhelpful), 0 (neutral), 1 (helpful)'),
388
- feedback: z.string().optional().describe('Optional text feedback'),
558
+ feedback: z.string().optional().describe('Optional feedback text'),
389
559
  },
390
560
  async (args) => {
391
561
  const result = await callRemoteServer('tools/call', {