@pragmatic-growth/memory-mcp 1.0.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.bak ADDED
@@ -0,0 +1,2 @@
1
+ @pragmatic-growth:registry=https://registry.npmjs.org/
2
+ //registry.npmjs.org/:_authToken=${NPM_TOKEN}
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # NT-Memory MCP (stdio)
2
+
3
+ Local MCP stdio server for NT-Memory tax knowledge base. Use this with Raycast and other stdio-only MCP clients.
4
+
5
+ ## Two Operating Modes
6
+
7
+ The server supports two modes for different use cases:
8
+
9
+ ### Read-Only Mode (Default)
10
+ Safe for general use - only search and read operations:
11
+ - `search_tax_knowledge` - Semantic search
12
+ - `answer_question` - RAG with auto-generation
13
+ - `get_article` - Get article by ID
14
+ - `list_articles` - Browse articles
15
+ - `list_categories` - List categories with counts
16
+ - `log_unanswered` - Flag gaps
17
+ - `health_check` - System status
18
+
19
+ ### Full Mode (with `--full` flag)
20
+ Includes all read-only tools PLUS write operations:
21
+ - `add_article` - Create new articles
22
+ - `edit_article` - Update existing articles
23
+ - `remove_article` - Soft-delete articles
24
+ - `rate_answer` - Rate previous answers
25
+
26
+ ## Installation
27
+
28
+ ### For Raycast (Read-Only)
29
+
30
+ ```json
31
+ {
32
+ "name": "nt-memory",
33
+ "type": "stdio",
34
+ "command": "npx",
35
+ "args": ["-y", "@anthropic/nt-memory-mcp"],
36
+ "env": {
37
+ "MCP_API_KEY": "your-api-key-here"
38
+ }
39
+ }
40
+ ```
41
+
42
+ ### For Raycast (Full Mode)
43
+
44
+ ```json
45
+ {
46
+ "name": "nt-memory-full",
47
+ "type": "stdio",
48
+ "command": "npx",
49
+ "args": ["-y", "@anthropic/nt-memory-mcp", "--full"],
50
+ "env": {
51
+ "MCP_API_KEY": "your-api-key-here"
52
+ }
53
+ }
54
+ ```
55
+
56
+ ### Global Installation
57
+
58
+ ```bash
59
+ npm install -g @anthropic/nt-memory-mcp
60
+ ```
61
+
62
+ Then run directly:
63
+
64
+ ```bash
65
+ # Read-only mode (default)
66
+ MCP_API_KEY=your-key nt-memory-mcp
67
+
68
+ # Full mode
69
+ MCP_API_KEY=your-key nt-memory-mcp --full
70
+ ```
71
+
72
+ ## Environment Variables
73
+
74
+ | Variable | Required | Description |
75
+ |----------|----------|-------------|
76
+ | `MCP_API_KEY` | Yes | API key for authentication |
77
+ | `MCP_SERVER_URL` | No | Custom server URL (default: production) |
78
+ | `MCP_MODE` | No | Set to `full` to enable write operations |
79
+
80
+ Note: The `--full` CLI flag takes precedence over `MCP_MODE` env var.
81
+
82
+ ## Available Tools
83
+
84
+ ### Core Tools (All Modes)
85
+
86
+ | Tool | Description |
87
+ |------|-------------|
88
+ | `search_tax_knowledge` | Search US non-resident tax knowledge base using semantic similarity |
89
+ | `answer_question` | Get AI-powered answers with sources (generates articles if needed) |
90
+ | `get_article` | Retrieve complete article content by ID |
91
+ | `list_articles` | Browse recent articles with optional category filter |
92
+ | `list_categories` | List all categories with article counts |
93
+ | `log_unanswered` | Flag questions as unanswered for gap analysis |
94
+ | `health_check` | Check system status, database connectivity, and current mode |
95
+
96
+ ### Full Mode Tools
97
+
98
+ | Tool | Description |
99
+ |------|-------------|
100
+ | `add_article` | Create a new article with automatic embedding generation |
101
+ | `edit_article` | Update article content, metadata, or category |
102
+ | `remove_article` | Soft-delete an article (preserves data for audit) |
103
+ | `rate_answer` | Rate previous answers: -1 (unhelpful), 0 (neutral), 1 (helpful) |
104
+
105
+ ## How It Works
106
+
107
+ This package runs locally as a stdio MCP server and proxies requests to the remote NT-Memory HTTP server on Railway. This allows Raycast (which only supports stdio) to use our cloud-hosted knowledge base.
108
+
109
+ ```
110
+ Raycast (stdio) → nt-memory-mcp (local) → HTTP → Railway (cloud)
111
+ ```
112
+
113
+ The server also respects the remote server's mode configuration. For full mode to work:
114
+ 1. The stdio proxy must have `--full` flag OR `MCP_MODE=full`
115
+ 2. The remote server must have `MCP_MODE=full` in its environment
116
+
117
+ ## License
118
+
119
+ MIT
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PG-Memory MCP Stdio Server
4
+ *
5
+ * This is a local stdio MCP server that proxies requests to the
6
+ * remote PG-Memory HTTP server on Railway.
7
+ *
8
+ * Usage:
9
+ * npx @pragmatic-growth/memory-mcp # Read-only mode (default)
10
+ * npx @pragmatic-growth/memory-mcp --full # Full mode with edit capabilities
11
+ * npx @pragmatic-growth/memory-mcp --edit # Alias for --full
12
+ *
13
+ * Environment variables:
14
+ * MCP_API_KEY - API key for authentication (required)
15
+ * MCP_SERVER_URL - Server URL (default: https://pg-memory-production.up.railway.app/api/mcp)
16
+ * MCP_MODE - Mode: 'readonly' (default) or 'full' for edit capabilities
17
+ */
18
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PG-Memory MCP Stdio Server
4
+ *
5
+ * This is a local stdio MCP server that proxies requests to the
6
+ * remote PG-Memory HTTP server on Railway.
7
+ *
8
+ * Usage:
9
+ * npx @pragmatic-growth/memory-mcp # Read-only mode (default)
10
+ * npx @pragmatic-growth/memory-mcp --full # Full mode with edit capabilities
11
+ * npx @pragmatic-growth/memory-mcp --edit # Alias for --full
12
+ *
13
+ * Environment variables:
14
+ * MCP_API_KEY - API key for authentication (required)
15
+ * MCP_SERVER_URL - Server URL (default: https://pg-memory-production.up.railway.app/api/mcp)
16
+ * MCP_MODE - Mode: 'readonly' (default) or 'full' for edit capabilities
17
+ */
18
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
19
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
20
+ import { z } from 'zod';
21
+ // Parse CLI arguments for mode
22
+ const args = process.argv.slice(2);
23
+ const hasFullFlag = args.includes('--full') || args.includes('--edit');
24
+ const envMode = process.env.MCP_MODE?.toLowerCase();
25
+ const isFullMode = hasFullFlag || envMode === 'full' || envMode === 'edit';
26
+ // Configuration
27
+ const SERVER_URL = process.env.MCP_SERVER_URL || 'https://pg-memory-production.up.railway.app/api/mcp';
28
+ const API_KEY = process.env.MCP_API_KEY;
29
+ if (!API_KEY) {
30
+ console.error('Error: MCP_API_KEY environment variable is required');
31
+ console.error('Set it in your Raycast MCP server configuration under "env"');
32
+ process.exit(1);
33
+ }
34
+ // HTTP session management
35
+ let sessionId = null;
36
+ /**
37
+ * Make an HTTP request to the remote MCP server.
38
+ */
39
+ async function callRemoteServer(method, params) {
40
+ const headers = {
41
+ 'Content-Type': 'application/json',
42
+ 'Authorization': `Bearer ${API_KEY}`,
43
+ };
44
+ if (sessionId) {
45
+ headers['Mcp-Session-Id'] = sessionId;
46
+ }
47
+ const body = {
48
+ jsonrpc: '2.0',
49
+ id: Date.now(),
50
+ method,
51
+ params,
52
+ };
53
+ const response = await fetch(SERVER_URL, {
54
+ method: 'POST',
55
+ headers,
56
+ body: JSON.stringify(body),
57
+ });
58
+ // Capture session ID from initialize response
59
+ const newSessionId = response.headers.get('mcp-session-id');
60
+ if (newSessionId) {
61
+ sessionId = newSessionId;
62
+ }
63
+ const result = await response.json();
64
+ if (result.error) {
65
+ throw new Error(result.error.message);
66
+ }
67
+ return result.result;
68
+ }
69
+ /**
70
+ * Initialize connection to remote server.
71
+ */
72
+ async function initializeRemoteSession() {
73
+ await callRemoteServer('initialize', {
74
+ protocolVersion: '2024-11-05',
75
+ capabilities: {},
76
+ clientInfo: {
77
+ name: 'pg-memory-stdio',
78
+ version: '1.0.0',
79
+ },
80
+ });
81
+ }
82
+ /**
83
+ * Create and start the stdio MCP server.
84
+ */
85
+ async function main() {
86
+ const modeLabel = isFullMode ? 'full' : 'readonly';
87
+ // Initialize remote session first
88
+ try {
89
+ await initializeRemoteSession();
90
+ }
91
+ catch (error) {
92
+ console.error('Failed to connect to remote server:', error);
93
+ process.exit(1);
94
+ }
95
+ // Create local MCP server
96
+ const server = new McpServer({
97
+ name: 'pg-memory',
98
+ version: '1.0.0',
99
+ description: `PG-Memory knowledge base (${modeLabel} mode)`,
100
+ });
101
+ // Register tools that proxy to remote server
102
+ // These match the tools registered on the remote server
103
+ server.tool('search_tax_knowledge', 'Search the US non-resident tax compliance knowledge base using semantic similarity', {
104
+ query: z.string().describe('The tax question or topic to search for'),
105
+ limit: z.number().optional().describe('Maximum number of results to return (default: 5)'),
106
+ threshold: z.number().optional().describe('Minimum similarity threshold 0-1 (default: 0.55)'),
107
+ }, async (args) => {
108
+ const result = await callRemoteServer('tools/call', {
109
+ name: 'search_tax_knowledge',
110
+ arguments: args,
111
+ });
112
+ return result;
113
+ });
114
+ server.tool('answer_question', 'Answer a tax question using the knowledge base with RAG', {
115
+ question: z.string().describe('The tax question to answer'),
116
+ context: z.string().optional().describe('Additional context'),
117
+ }, async (args) => {
118
+ const result = await callRemoteServer('tools/call', {
119
+ name: 'answer_question',
120
+ arguments: args,
121
+ });
122
+ return result;
123
+ });
124
+ server.tool('get_article', 'Retrieve the complete content of a knowledge base article by its ID', {
125
+ article_id: z.string().describe('The article ID (e.g., art_abc123)'),
126
+ }, async (args) => {
127
+ const result = await callRemoteServer('tools/call', {
128
+ name: 'get_article',
129
+ arguments: args,
130
+ });
131
+ return result;
132
+ });
133
+ server.tool('list_articles', 'List recent articles from the knowledge base', {
134
+ limit: z.number().optional().describe('Maximum articles (default: 10)'),
135
+ category: z.string().optional().describe('Filter by category'),
136
+ }, async (args) => {
137
+ const result = await callRemoteServer('tools/call', {
138
+ name: 'list_articles',
139
+ arguments: args,
140
+ });
141
+ return result;
142
+ });
143
+ server.tool('log_unanswered', 'Explicitly flag a question as unanswered for gap analysis', {
144
+ query: z.string().describe('The question that could not be answered'),
145
+ reason: z.string().optional().describe('Reason why the question could not be answered'),
146
+ }, async (args) => {
147
+ const result = await callRemoteServer('tools/call', {
148
+ name: 'log_unanswered',
149
+ arguments: args,
150
+ });
151
+ return result;
152
+ });
153
+ server.tool('health_check', 'Check the health status of the knowledge base system', {}, async () => {
154
+ const result = await callRemoteServer('tools/call', {
155
+ name: 'health_check',
156
+ arguments: {},
157
+ });
158
+ return result;
159
+ });
160
+ // Tool 7: List Categories (available in all modes)
161
+ server.tool('list_categories', 'List all categories in the knowledge base with article counts', {}, async () => {
162
+ const result = await callRemoteServer('tools/call', {
163
+ name: 'list_categories',
164
+ arguments: {},
165
+ });
166
+ return result;
167
+ });
168
+ // === FULL MODE TOOLS ===
169
+ // These tools are only available when --full flag is passed or MCP_MODE=full
170
+ if (isFullMode) {
171
+ // Tool 8: Add Article
172
+ server.tool('add_article', 'Create a new article in the knowledge base (full mode only)', {
173
+ title: z.string().describe('Article title'),
174
+ content: z.string().describe('Article content in markdown format'),
175
+ summary: z.string().optional().describe('Brief summary (auto-generated if not provided)'),
176
+ category: z.string().optional().describe('Category (e.g., federal-tax, state-compliance)'),
177
+ tags: z.string().optional().describe('Comma-separated tags'),
178
+ source: z.string().optional().describe('Source of the article (e.g., manual, irs.gov)'),
179
+ }, async (args) => {
180
+ const result = await callRemoteServer('tools/call', {
181
+ name: 'add_article',
182
+ arguments: args,
183
+ });
184
+ return result;
185
+ });
186
+ // Tool 9: Edit Article
187
+ server.tool('edit_article', 'Update an existing article in the knowledge base (full mode only)', {
188
+ article_id: z.string().describe('Article ID to update'),
189
+ title: z.string().optional().describe('New title'),
190
+ content: z.string().optional().describe('New content'),
191
+ summary: z.string().optional().describe('New summary'),
192
+ category: z.string().optional().describe('New category'),
193
+ tags: z.string().optional().describe('New comma-separated tags'),
194
+ }, async (args) => {
195
+ const result = await callRemoteServer('tools/call', {
196
+ name: 'edit_article',
197
+ arguments: args,
198
+ });
199
+ return result;
200
+ });
201
+ // Tool 10: Remove Article
202
+ server.tool('remove_article', 'Soft-delete an article from the knowledge base (full mode only)', {
203
+ article_id: z.string().describe('Article ID to remove'),
204
+ reason: z.string().optional().describe('Reason for deletion (for audit log)'),
205
+ }, async (args) => {
206
+ const result = await callRemoteServer('tools/call', {
207
+ name: 'remove_article',
208
+ arguments: args,
209
+ });
210
+ return result;
211
+ });
212
+ // Tool 11: Rate Answer
213
+ server.tool('rate_answer', 'Rate a previous answer as helpful, unhelpful, or neutral (full mode only)', {
214
+ query_id: z.string().describe('Query log ID from a previous answer_question response'),
215
+ rating: z.number().describe('Rating: -1 (unhelpful), 0 (neutral), 1 (helpful)'),
216
+ feedback: z.string().optional().describe('Optional text feedback'),
217
+ }, async (args) => {
218
+ const result = await callRemoteServer('tools/call', {
219
+ name: 'rate_answer',
220
+ arguments: args,
221
+ });
222
+ return result;
223
+ });
224
+ }
225
+ // Connect via stdio transport
226
+ const transport = new StdioServerTransport();
227
+ await server.connect(transport);
228
+ }
229
+ main().catch((error) => {
230
+ console.error('Fatal error:', error);
231
+ process.exit(1);
232
+ });
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@pragmatic-growth/memory-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP stdio client for PG-Memory knowledge base - connect AI agents to your PostgreSQL knowledge base via Model Context Protocol",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "memory-mcp": "./dist/index.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "dev": "tsx src/index.ts"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "model-context-protocol",
18
+ "raycast",
19
+ "knowledge-base",
20
+ "postgresql",
21
+ "pgvector",
22
+ "ai",
23
+ "rag"
24
+ ],
25
+ "author": "Pragmatic Growth",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.25.1",
29
+ "zod": "^3.24.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^20",
33
+ "tsx": "^4.19.0",
34
+ "typescript": "^5"
35
+ },
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/pragmaticgrowth/pg-memory.git",
42
+ "directory": "packages/mcp-stdio"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ }
47
+ }
package/src/index.ts ADDED
@@ -0,0 +1,326 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PG-Memory MCP Stdio Server
4
+ *
5
+ * This is a local stdio MCP server that proxies requests to the
6
+ * remote PG-Memory HTTP server on Railway.
7
+ *
8
+ * Usage:
9
+ * npx @pragmatic-growth/memory-mcp # Read-only mode (default)
10
+ * npx @pragmatic-growth/memory-mcp --full # Full mode with edit capabilities
11
+ * npx @pragmatic-growth/memory-mcp --edit # Alias for --full
12
+ *
13
+ * Environment variables:
14
+ * MCP_API_KEY - API key for authentication (required)
15
+ * MCP_SERVER_URL - Server URL (default: https://pg-memory-production.up.railway.app/api/mcp)
16
+ * MCP_MODE - Mode: 'readonly' (default) or 'full' for edit capabilities
17
+ */
18
+
19
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
20
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
21
+ import { z } from 'zod';
22
+
23
+ // Parse CLI arguments for mode
24
+ const args = process.argv.slice(2);
25
+ const hasFullFlag = args.includes('--full') || args.includes('--edit');
26
+ const envMode = process.env.MCP_MODE?.toLowerCase();
27
+ const isFullMode = hasFullFlag || envMode === 'full' || envMode === 'edit';
28
+
29
+ // Configuration
30
+ const SERVER_URL = process.env.MCP_SERVER_URL || 'https://pg-memory-production.up.railway.app/api/mcp';
31
+ const API_KEY = process.env.MCP_API_KEY;
32
+
33
+ if (!API_KEY) {
34
+ console.error('Error: MCP_API_KEY environment variable is required');
35
+ console.error('Set it in your Raycast MCP server configuration under "env"');
36
+ process.exit(1);
37
+ }
38
+
39
+ // HTTP session management
40
+ let sessionId: string | null = null;
41
+
42
+ interface JsonRpcResponse {
43
+ result?: {
44
+ content?: Array<{ type: string; text: string }>;
45
+ [key: string]: unknown;
46
+ };
47
+ error?: { message: string };
48
+ }
49
+
50
+ /**
51
+ * Make an HTTP request to the remote MCP server.
52
+ */
53
+ async function callRemoteServer(method: string, params?: Record<string, unknown>): Promise<unknown> {
54
+ const headers: Record<string, string> = {
55
+ 'Content-Type': 'application/json',
56
+ 'Authorization': `Bearer ${API_KEY}`,
57
+ };
58
+
59
+ if (sessionId) {
60
+ headers['Mcp-Session-Id'] = sessionId;
61
+ }
62
+
63
+ const body = {
64
+ jsonrpc: '2.0',
65
+ id: Date.now(),
66
+ method,
67
+ params,
68
+ };
69
+
70
+ const response = await fetch(SERVER_URL, {
71
+ method: 'POST',
72
+ headers,
73
+ body: JSON.stringify(body),
74
+ });
75
+
76
+ // Capture session ID from initialize response
77
+ const newSessionId = response.headers.get('mcp-session-id');
78
+ if (newSessionId) {
79
+ sessionId = newSessionId;
80
+ }
81
+
82
+ const result = await response.json() as JsonRpcResponse;
83
+
84
+ if (result.error) {
85
+ throw new Error(result.error.message);
86
+ }
87
+
88
+ return result.result;
89
+ }
90
+
91
+ /**
92
+ * Initialize connection to remote server.
93
+ */
94
+ async function initializeRemoteSession(): Promise<void> {
95
+ await callRemoteServer('initialize', {
96
+ protocolVersion: '2024-11-05',
97
+ capabilities: {},
98
+ clientInfo: {
99
+ name: 'pg-memory-stdio',
100
+ version: '1.0.0',
101
+ },
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Create and start the stdio MCP server.
107
+ */
108
+ async function main(): Promise<void> {
109
+ const modeLabel = isFullMode ? 'full' : 'readonly';
110
+
111
+ // Initialize remote session first
112
+ try {
113
+ await initializeRemoteSession();
114
+ } catch (error) {
115
+ console.error('Failed to connect to remote server:', error);
116
+ process.exit(1);
117
+ }
118
+
119
+ // Create local MCP server
120
+ const server = new McpServer({
121
+ name: 'pg-memory',
122
+ version: '1.0.0',
123
+ description: `PG-Memory knowledge base (${modeLabel} mode)`,
124
+ });
125
+
126
+ // Register tools that proxy to remote server
127
+ // These match the tools registered on the remote server
128
+
129
+ server.tool(
130
+ 'search_tax_knowledge',
131
+ 'Search the US non-resident tax compliance knowledge base using semantic similarity',
132
+ {
133
+ query: z.string().describe('The tax question or topic to search for'),
134
+ limit: z.number().optional().describe('Maximum number of results to return (default: 5)'),
135
+ threshold: z.number().optional().describe('Minimum similarity threshold 0-1 (default: 0.55)'),
136
+ },
137
+ async (args) => {
138
+ const result = await callRemoteServer('tools/call', {
139
+ name: 'search_tax_knowledge',
140
+ arguments: args,
141
+ }) as { content: Array<{ type: 'text'; text: string }> };
142
+ return result;
143
+ }
144
+ );
145
+
146
+ server.tool(
147
+ 'answer_question',
148
+ 'Answer a tax question using the knowledge base with RAG',
149
+ {
150
+ question: z.string().describe('The tax question to answer'),
151
+ context: z.string().optional().describe('Additional context'),
152
+ },
153
+ async (args) => {
154
+ const result = await callRemoteServer('tools/call', {
155
+ name: 'answer_question',
156
+ arguments: args,
157
+ }) as { content: Array<{ type: 'text'; text: string }> };
158
+ return result;
159
+ }
160
+ );
161
+
162
+ server.tool(
163
+ 'get_article',
164
+ 'Retrieve the complete content of a knowledge base article by its ID',
165
+ {
166
+ article_id: z.string().describe('The article ID (e.g., art_abc123)'),
167
+ },
168
+ async (args) => {
169
+ const result = await callRemoteServer('tools/call', {
170
+ name: 'get_article',
171
+ arguments: args,
172
+ }) as { content: Array<{ type: 'text'; text: string }> };
173
+ return result;
174
+ }
175
+ );
176
+
177
+ server.tool(
178
+ 'list_articles',
179
+ 'List recent articles from the knowledge base',
180
+ {
181
+ limit: z.number().optional().describe('Maximum articles (default: 10)'),
182
+ category: z.string().optional().describe('Filter by category'),
183
+ },
184
+ async (args) => {
185
+ const result = await callRemoteServer('tools/call', {
186
+ name: 'list_articles',
187
+ arguments: args,
188
+ }) as { content: Array<{ type: 'text'; text: string }> };
189
+ return result;
190
+ }
191
+ );
192
+
193
+ server.tool(
194
+ 'log_unanswered',
195
+ 'Explicitly flag a question as unanswered for gap analysis',
196
+ {
197
+ query: z.string().describe('The question that could not be answered'),
198
+ reason: z.string().optional().describe('Reason why the question could not be answered'),
199
+ },
200
+ async (args) => {
201
+ const result = await callRemoteServer('tools/call', {
202
+ name: 'log_unanswered',
203
+ arguments: args,
204
+ }) as { content: Array<{ type: 'text'; text: string }> };
205
+ return result;
206
+ }
207
+ );
208
+
209
+ server.tool(
210
+ 'health_check',
211
+ 'Check the health status of the knowledge base system',
212
+ {},
213
+ async () => {
214
+ const result = await callRemoteServer('tools/call', {
215
+ name: 'health_check',
216
+ arguments: {},
217
+ }) as { content: Array<{ type: 'text'; text: string }> };
218
+ return result;
219
+ }
220
+ );
221
+
222
+ // Tool 7: List Categories (available in all modes)
223
+ server.tool(
224
+ 'list_categories',
225
+ 'List all categories in the knowledge base with article counts',
226
+ {},
227
+ async () => {
228
+ const result = await callRemoteServer('tools/call', {
229
+ name: 'list_categories',
230
+ arguments: {},
231
+ }) as { content: Array<{ type: 'text'; text: string }> };
232
+ return result;
233
+ }
234
+ );
235
+
236
+ // === FULL MODE TOOLS ===
237
+ // These tools are only available when --full flag is passed or MCP_MODE=full
238
+
239
+ if (isFullMode) {
240
+ // Tool 8: Add Article
241
+ server.tool(
242
+ 'add_article',
243
+ 'Create a new article in the knowledge base (full mode only)',
244
+ {
245
+ title: z.string().describe('Article title'),
246
+ content: z.string().describe('Article content in markdown format'),
247
+ summary: z.string().optional().describe('Brief summary (auto-generated if not provided)'),
248
+ category: z.string().optional().describe('Category (e.g., federal-tax, state-compliance)'),
249
+ tags: z.string().optional().describe('Comma-separated tags'),
250
+ source: z.string().optional().describe('Source of the article (e.g., manual, irs.gov)'),
251
+ },
252
+ async (args) => {
253
+ const result = await callRemoteServer('tools/call', {
254
+ name: 'add_article',
255
+ arguments: args,
256
+ }) as { content: Array<{ type: 'text'; text: string }> };
257
+ return result;
258
+ }
259
+ );
260
+
261
+ // Tool 9: Edit Article
262
+ server.tool(
263
+ 'edit_article',
264
+ 'Update an existing article in the knowledge base (full mode only)',
265
+ {
266
+ article_id: z.string().describe('Article ID to update'),
267
+ title: z.string().optional().describe('New title'),
268
+ content: z.string().optional().describe('New content'),
269
+ summary: z.string().optional().describe('New summary'),
270
+ category: z.string().optional().describe('New category'),
271
+ tags: z.string().optional().describe('New comma-separated tags'),
272
+ },
273
+ async (args) => {
274
+ const result = await callRemoteServer('tools/call', {
275
+ name: 'edit_article',
276
+ arguments: args,
277
+ }) as { content: Array<{ type: 'text'; text: string }> };
278
+ return result;
279
+ }
280
+ );
281
+
282
+ // Tool 10: Remove Article
283
+ server.tool(
284
+ 'remove_article',
285
+ 'Soft-delete an article from the knowledge base (full mode only)',
286
+ {
287
+ article_id: z.string().describe('Article ID to remove'),
288
+ reason: z.string().optional().describe('Reason for deletion (for audit log)'),
289
+ },
290
+ async (args) => {
291
+ const result = await callRemoteServer('tools/call', {
292
+ name: 'remove_article',
293
+ arguments: args,
294
+ }) as { content: Array<{ type: 'text'; text: string }> };
295
+ return result;
296
+ }
297
+ );
298
+
299
+ // Tool 11: Rate Answer
300
+ server.tool(
301
+ 'rate_answer',
302
+ 'Rate a previous answer as helpful, unhelpful, or neutral (full mode only)',
303
+ {
304
+ query_id: z.string().describe('Query log ID from a previous answer_question response'),
305
+ rating: z.number().describe('Rating: -1 (unhelpful), 0 (neutral), 1 (helpful)'),
306
+ feedback: z.string().optional().describe('Optional text feedback'),
307
+ },
308
+ async (args) => {
309
+ const result = await callRemoteServer('tools/call', {
310
+ name: 'rate_answer',
311
+ arguments: args,
312
+ }) as { content: Array<{ type: 'text'; text: string }> };
313
+ return result;
314
+ }
315
+ );
316
+ }
317
+
318
+ // Connect via stdio transport
319
+ const transport = new StdioServerTransport();
320
+ await server.connect(transport);
321
+ }
322
+
323
+ main().catch((error) => {
324
+ console.error('Fatal error:', error);
325
+ process.exit(1);
326
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules", "dist"]
15
+ }