midnight-mcp 0.0.5 → 0.0.6
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/README.md +67 -7
- package/dist/tools/search.js +116 -130
- package/dist/utils/errors.js +2 -2
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
# Midnight MCP Server
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/midnight-mcp)
|
|
4
|
+
[](https://github.com/Olanetsoft/midnight-mcp/actions/workflows/index.yml)
|
|
5
|
+
[](https://github.com/Olanetsoft/midnight-mcp/actions/workflows/ci.yml)
|
|
6
|
+
|
|
3
7
|
MCP server that gives AI assistants access to Midnight blockchain—search contracts, analyze code, and explore documentation.
|
|
4
8
|
|
|
5
9
|
## Quick Start
|
|
6
10
|
|
|
11
|
+
### Claude Desktop
|
|
12
|
+
|
|
7
13
|
Add to your `claude_desktop_config.json`:
|
|
8
14
|
|
|
9
15
|
```json
|
|
@@ -17,7 +23,61 @@ Add to your `claude_desktop_config.json`:
|
|
|
17
23
|
}
|
|
18
24
|
```
|
|
19
25
|
|
|
20
|
-
|
|
26
|
+
<details>
|
|
27
|
+
<summary><strong>📍 Config file locations</strong></summary>
|
|
28
|
+
|
|
29
|
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
30
|
+
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
31
|
+
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
|
32
|
+
|
|
33
|
+
</details>
|
|
34
|
+
|
|
35
|
+
### Cursor
|
|
36
|
+
|
|
37
|
+
Add to Cursor's MCP settings (Settings → MCP → Add Server):
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"midnight": {
|
|
43
|
+
"command": "npx",
|
|
44
|
+
"args": ["-y", "midnight-mcp"]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or add to `.cursor/mcp.json` in your project:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"mcpServers": {
|
|
55
|
+
"midnight": {
|
|
56
|
+
"command": "npx",
|
|
57
|
+
"args": ["-y", "midnight-mcp"]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Windsurf
|
|
64
|
+
|
|
65
|
+
Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"mcpServers": {
|
|
70
|
+
"midnight": {
|
|
71
|
+
"command": "npx",
|
|
72
|
+
"args": ["-y", "midnight-mcp"]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
Restart your editor after adding the config. All features work out of the box—no API keys or setup required.
|
|
21
81
|
|
|
22
82
|
---
|
|
23
83
|
|
|
@@ -86,13 +146,13 @@ Add `"GITHUB_TOKEN": "ghp_..."` for higher GitHub API rate limits (60 → 5000 r
|
|
|
86
146
|
- `midnight://code/*` — Examples, patterns, and templates
|
|
87
147
|
- `midnight://schema/*` — AST, transaction, and proof schemas
|
|
88
148
|
|
|
89
|
-
### Prompts
|
|
149
|
+
### Prompts (5)
|
|
90
150
|
|
|
91
|
-
- `midnight
|
|
92
|
-
- `midnight
|
|
93
|
-
- `midnight
|
|
94
|
-
- `midnight
|
|
95
|
-
- `midnight
|
|
151
|
+
- `midnight:create-contract` — Create new contracts
|
|
152
|
+
- `midnight:review-contract` — Security review
|
|
153
|
+
- `midnight:explain-concept` — Learn Midnight concepts
|
|
154
|
+
- `midnight:compare-approaches` — Compare implementation approaches
|
|
155
|
+
- `midnight:debug-contract` — Debug issues
|
|
96
156
|
|
|
97
157
|
---
|
|
98
158
|
|
package/dist/tools/search.js
CHANGED
|
@@ -1,6 +1,86 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { vectorStore } from "../db/index.js";
|
|
3
3
|
import { logger, validateQuery, validateNumber, searchCache, createCacheKey, isHostedMode, searchCompactHosted, searchTypeScriptHosted, searchDocsHosted, } from "../utils/index.js";
|
|
4
|
+
/**
|
|
5
|
+
* Validate and prepare common search parameters
|
|
6
|
+
* Extracts common validation logic used by all search functions
|
|
7
|
+
*/
|
|
8
|
+
function validateSearchInput(query, limit) {
|
|
9
|
+
const queryValidation = validateQuery(query);
|
|
10
|
+
if (!queryValidation.isValid) {
|
|
11
|
+
return {
|
|
12
|
+
success: false,
|
|
13
|
+
error: {
|
|
14
|
+
error: "Invalid query",
|
|
15
|
+
details: queryValidation.errors,
|
|
16
|
+
suggestion: "Provide a valid search query with at least 2 characters",
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const limitValidation = validateNumber(limit, {
|
|
21
|
+
min: 1,
|
|
22
|
+
max: 50,
|
|
23
|
+
defaultValue: 10,
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
success: true,
|
|
27
|
+
context: {
|
|
28
|
+
sanitizedQuery: queryValidation.sanitized,
|
|
29
|
+
limit: limitValidation.value,
|
|
30
|
+
warnings: queryValidation.warnings,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check cache for existing search results
|
|
36
|
+
*/
|
|
37
|
+
function checkSearchCache(cacheKey) {
|
|
38
|
+
const cached = searchCache.get(cacheKey);
|
|
39
|
+
if (cached) {
|
|
40
|
+
logger.debug("Search cache hit", { cacheKey });
|
|
41
|
+
return cached;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Execute hosted search with fallback handling
|
|
47
|
+
*/
|
|
48
|
+
async function tryHostedSearch(searchType, hostedSearchFn, cacheKey, warnings) {
|
|
49
|
+
if (!isHostedMode()) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const response = await hostedSearchFn();
|
|
54
|
+
searchCache.set(cacheKey, response);
|
|
55
|
+
return {
|
|
56
|
+
result: {
|
|
57
|
+
...response,
|
|
58
|
+
...(warnings.length > 0 && { warnings }),
|
|
59
|
+
},
|
|
60
|
+
cached: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
logger.warn(`Hosted API ${searchType} search failed, falling back to local`, {
|
|
65
|
+
error: String(error),
|
|
66
|
+
});
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Add warnings to response and cache it
|
|
72
|
+
*/
|
|
73
|
+
function finalizeResponse(response, cacheKey, warnings) {
|
|
74
|
+
const finalResponse = {
|
|
75
|
+
...response,
|
|
76
|
+
...(warnings.length > 0 && { warnings }),
|
|
77
|
+
};
|
|
78
|
+
searchCache.set(cacheKey, finalResponse);
|
|
79
|
+
return finalResponse;
|
|
80
|
+
}
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// Schema Definitions
|
|
83
|
+
// ============================================================================
|
|
4
84
|
// Schema definitions for tool inputs
|
|
5
85
|
export const SearchCompactInputSchema = z.object({
|
|
6
86
|
query: z.string().describe("Natural language search query for Compact code"),
|
|
@@ -44,52 +124,25 @@ export const SearchDocsInputSchema = z.object({
|
|
|
44
124
|
* Search Compact smart contract code and patterns
|
|
45
125
|
*/
|
|
46
126
|
export async function searchCompact(input) {
|
|
47
|
-
// Validate input
|
|
48
|
-
const
|
|
49
|
-
if (!
|
|
50
|
-
return
|
|
51
|
-
error: "Invalid query",
|
|
52
|
-
details: queryValidation.errors,
|
|
53
|
-
suggestion: "Provide a valid search query with at least 2 characters",
|
|
54
|
-
};
|
|
127
|
+
// Validate input using common helper
|
|
128
|
+
const validation = validateSearchInput(input.query, input.limit);
|
|
129
|
+
if (!validation.success) {
|
|
130
|
+
return validation.error;
|
|
55
131
|
}
|
|
56
|
-
const
|
|
57
|
-
min: 1,
|
|
58
|
-
max: 50,
|
|
59
|
-
defaultValue: 10,
|
|
60
|
-
});
|
|
61
|
-
const sanitizedQuery = queryValidation.sanitized;
|
|
62
|
-
const limit = limitValidation.value;
|
|
132
|
+
const { sanitizedQuery, limit, warnings } = validation.context;
|
|
63
133
|
logger.debug("Searching Compact code", {
|
|
64
134
|
query: sanitizedQuery,
|
|
65
135
|
mode: isHostedMode() ? "hosted" : "local",
|
|
66
136
|
});
|
|
67
137
|
// Check cache first
|
|
68
138
|
const cacheKey = createCacheKey("compact", sanitizedQuery, limit, input.filter?.repository);
|
|
69
|
-
const cached =
|
|
70
|
-
if (cached)
|
|
71
|
-
logger.debug("Search cache hit", { cacheKey });
|
|
139
|
+
const cached = checkSearchCache(cacheKey);
|
|
140
|
+
if (cached)
|
|
72
141
|
return cached;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
const response = await searchCompactHosted(sanitizedQuery, limit);
|
|
78
|
-
searchCache.set(cacheKey, response);
|
|
79
|
-
return {
|
|
80
|
-
...response,
|
|
81
|
-
...(queryValidation.warnings.length > 0 && {
|
|
82
|
-
warnings: queryValidation.warnings,
|
|
83
|
-
}),
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
catch (error) {
|
|
87
|
-
logger.warn("Hosted API search failed, falling back to local", {
|
|
88
|
-
error: String(error),
|
|
89
|
-
});
|
|
90
|
-
// Fall through to local search
|
|
91
|
-
}
|
|
92
|
-
}
|
|
142
|
+
// Try hosted API first
|
|
143
|
+
const hostedResult = await tryHostedSearch("compact", () => searchCompactHosted(sanitizedQuery, limit), cacheKey, warnings);
|
|
144
|
+
if (hostedResult)
|
|
145
|
+
return hostedResult.result;
|
|
93
146
|
// Local search (fallback or when in local mode)
|
|
94
147
|
const filter = {
|
|
95
148
|
language: "compact",
|
|
@@ -110,64 +163,32 @@ export async function searchCompact(input) {
|
|
|
110
163
|
})),
|
|
111
164
|
totalResults: results.length,
|
|
112
165
|
query: sanitizedQuery,
|
|
113
|
-
...(queryValidation.warnings.length > 0 && {
|
|
114
|
-
warnings: queryValidation.warnings,
|
|
115
|
-
}),
|
|
116
166
|
};
|
|
117
|
-
|
|
118
|
-
searchCache.set(cacheKey, response);
|
|
119
|
-
return response;
|
|
167
|
+
return finalizeResponse(response, cacheKey, warnings);
|
|
120
168
|
}
|
|
121
169
|
/**
|
|
122
170
|
* Search TypeScript SDK code, types, and API implementations
|
|
123
171
|
*/
|
|
124
172
|
export async function searchTypeScript(input) {
|
|
125
|
-
// Validate input
|
|
126
|
-
const
|
|
127
|
-
if (!
|
|
128
|
-
return
|
|
129
|
-
error: "Invalid query",
|
|
130
|
-
details: queryValidation.errors,
|
|
131
|
-
suggestion: "Provide a valid search query with at least 2 characters",
|
|
132
|
-
};
|
|
173
|
+
// Validate input using common helper
|
|
174
|
+
const validation = validateSearchInput(input.query, input.limit);
|
|
175
|
+
if (!validation.success) {
|
|
176
|
+
return validation.error;
|
|
133
177
|
}
|
|
134
|
-
const
|
|
135
|
-
min: 1,
|
|
136
|
-
max: 50,
|
|
137
|
-
defaultValue: 10,
|
|
138
|
-
});
|
|
139
|
-
const sanitizedQuery = queryValidation.sanitized;
|
|
140
|
-
const limit = limitValidation.value;
|
|
178
|
+
const { sanitizedQuery, limit, warnings } = validation.context;
|
|
141
179
|
logger.debug("Searching TypeScript code", {
|
|
142
180
|
query: sanitizedQuery,
|
|
143
181
|
mode: isHostedMode() ? "hosted" : "local",
|
|
144
182
|
});
|
|
145
183
|
// Check cache
|
|
146
184
|
const cacheKey = createCacheKey("typescript", sanitizedQuery, limit, input.includeTypes, input.includeExamples);
|
|
147
|
-
const cached =
|
|
148
|
-
if (cached)
|
|
149
|
-
logger.debug("Search cache hit", { cacheKey });
|
|
185
|
+
const cached = checkSearchCache(cacheKey);
|
|
186
|
+
if (cached)
|
|
150
187
|
return cached;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
const response = await searchTypeScriptHosted(sanitizedQuery, limit, input.includeTypes);
|
|
156
|
-
searchCache.set(cacheKey, response);
|
|
157
|
-
return {
|
|
158
|
-
...response,
|
|
159
|
-
...(queryValidation.warnings.length > 0 && {
|
|
160
|
-
warnings: queryValidation.warnings,
|
|
161
|
-
}),
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
catch (error) {
|
|
165
|
-
logger.warn("Hosted API search failed, falling back to local", {
|
|
166
|
-
error: String(error),
|
|
167
|
-
});
|
|
168
|
-
// Fall through to local search
|
|
169
|
-
}
|
|
170
|
-
}
|
|
188
|
+
// Try hosted API first
|
|
189
|
+
const hostedResult = await tryHostedSearch("typescript", () => searchTypeScriptHosted(sanitizedQuery, limit, input.includeTypes), cacheKey, warnings);
|
|
190
|
+
if (hostedResult)
|
|
191
|
+
return hostedResult.result;
|
|
171
192
|
// Local search (fallback or when in local mode)
|
|
172
193
|
const filter = {
|
|
173
194
|
language: "typescript",
|
|
@@ -193,63 +214,32 @@ export async function searchTypeScript(input) {
|
|
|
193
214
|
})),
|
|
194
215
|
totalResults: filteredResults.length,
|
|
195
216
|
query: sanitizedQuery,
|
|
196
|
-
...(queryValidation.warnings.length > 0 && {
|
|
197
|
-
warnings: queryValidation.warnings,
|
|
198
|
-
}),
|
|
199
217
|
};
|
|
200
|
-
|
|
201
|
-
return response;
|
|
218
|
+
return finalizeResponse(response, cacheKey, warnings);
|
|
202
219
|
}
|
|
203
220
|
/**
|
|
204
221
|
* Full-text search across official Midnight documentation
|
|
205
222
|
*/
|
|
206
223
|
export async function searchDocs(input) {
|
|
207
|
-
// Validate input
|
|
208
|
-
const
|
|
209
|
-
if (!
|
|
210
|
-
return
|
|
211
|
-
error: "Invalid query",
|
|
212
|
-
details: queryValidation.errors,
|
|
213
|
-
suggestion: "Provide a valid search query with at least 2 characters",
|
|
214
|
-
};
|
|
224
|
+
// Validate input using common helper
|
|
225
|
+
const validation = validateSearchInput(input.query, input.limit);
|
|
226
|
+
if (!validation.success) {
|
|
227
|
+
return validation.error;
|
|
215
228
|
}
|
|
216
|
-
const
|
|
217
|
-
min: 1,
|
|
218
|
-
max: 50,
|
|
219
|
-
defaultValue: 10,
|
|
220
|
-
});
|
|
221
|
-
const sanitizedQuery = queryValidation.sanitized;
|
|
222
|
-
const limit = limitValidation.value;
|
|
229
|
+
const { sanitizedQuery, limit, warnings } = validation.context;
|
|
223
230
|
logger.debug("Searching documentation", {
|
|
224
231
|
query: sanitizedQuery,
|
|
225
232
|
mode: isHostedMode() ? "hosted" : "local",
|
|
226
233
|
});
|
|
227
234
|
// Check cache
|
|
228
235
|
const cacheKey = createCacheKey("docs", sanitizedQuery, limit, input.category);
|
|
229
|
-
const cached =
|
|
230
|
-
if (cached)
|
|
231
|
-
logger.debug("Search cache hit", { cacheKey });
|
|
236
|
+
const cached = checkSearchCache(cacheKey);
|
|
237
|
+
if (cached)
|
|
232
238
|
return cached;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
const response = await searchDocsHosted(sanitizedQuery, limit, input.category);
|
|
238
|
-
searchCache.set(cacheKey, response);
|
|
239
|
-
return {
|
|
240
|
-
...response,
|
|
241
|
-
...(queryValidation.warnings.length > 0 && {
|
|
242
|
-
warnings: queryValidation.warnings,
|
|
243
|
-
}),
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
catch (error) {
|
|
247
|
-
logger.warn("Hosted API search failed, falling back to local", {
|
|
248
|
-
error: String(error),
|
|
249
|
-
});
|
|
250
|
-
// Fall through to local search
|
|
251
|
-
}
|
|
252
|
-
}
|
|
239
|
+
// Try hosted API first
|
|
240
|
+
const hostedResult = await tryHostedSearch("docs", () => searchDocsHosted(sanitizedQuery, limit, input.category), cacheKey, warnings);
|
|
241
|
+
if (hostedResult)
|
|
242
|
+
return hostedResult.result;
|
|
253
243
|
// Local search (fallback or when in local mode)
|
|
254
244
|
const filter = {
|
|
255
245
|
language: "markdown",
|
|
@@ -273,12 +263,8 @@ export async function searchDocs(input) {
|
|
|
273
263
|
totalResults: results.length,
|
|
274
264
|
query: sanitizedQuery,
|
|
275
265
|
category: input.category,
|
|
276
|
-
...(queryValidation.warnings.length > 0 && {
|
|
277
|
-
warnings: queryValidation.warnings,
|
|
278
|
-
}),
|
|
279
266
|
};
|
|
280
|
-
|
|
281
|
-
return response;
|
|
267
|
+
return finalizeResponse(response, cacheKey, warnings);
|
|
282
268
|
}
|
|
283
269
|
// Tool definitions for MCP
|
|
284
270
|
export const searchTools = [
|
package/dist/utils/errors.js
CHANGED
|
@@ -69,8 +69,8 @@ export function createUserError(error, context) {
|
|
|
69
69
|
return new MCPError(`OpenAI API error${ctx}`, ErrorCodes.OPENAI_UNAVAILABLE, "OpenAI is optional. Without it, search uses keyword matching. " +
|
|
70
70
|
"To enable semantic search, add OPENAI_API_KEY to your config.");
|
|
71
71
|
}
|
|
72
|
-
// Default error
|
|
73
|
-
return new MCPError(`An error occurred${ctx}
|
|
72
|
+
// Default error - don't leak internal details
|
|
73
|
+
return new MCPError(`An error occurred${ctx}`, "UNKNOWN_ERROR", "If this problem persists, please report it at https://github.com/Olanetsoft/midnight-mcp/issues");
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
76
|
* Format error for MCP response
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "midnight-mcp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Model Context Protocol Server for Midnight Blockchain Development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
"dev": "tsx watch src/index.ts",
|
|
14
14
|
"test": "vitest",
|
|
15
15
|
"test:coverage": "vitest --coverage",
|
|
16
|
-
"lint": "eslint src/**/*.ts",
|
|
17
16
|
"format": "prettier --write src/**/*.ts",
|
|
18
17
|
"prepublishOnly": "npm run build"
|
|
19
18
|
},
|