@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 +2 -0
- package/README.md +119 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +232 -0
- package/package.json +47 -0
- package/src/index.ts +326 -0
- package/tsconfig.json +15 -0
package/.npmrc.bak
ADDED
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|