midnight-mcp 0.0.5 → 0.0.7

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 CHANGED
@@ -1,9 +1,15 @@
1
1
  # Midnight MCP Server
2
2
 
3
+ [![npm version](https://badge.fury.io/js/midnight-mcp.svg)](https://www.npmjs.com/package/midnight-mcp)
4
+ [![Index Repositories](https://github.com/Olanetsoft/midnight-mcp/actions/workflows/index.yml/badge.svg)](https://github.com/Olanetsoft/midnight-mcp/actions/workflows/index.yml)
5
+ [![CI](https://github.com/Olanetsoft/midnight-mcp/actions/workflows/ci.yml/badge.svg)](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
- Restart Claude Desktop. All features work out of the box—no API keys or setup required.
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-create-contract` — Create new contracts
92
- - `midnight-review-contract` — Security review
93
- - `midnight-explain-concept` — Learn Midnight concepts
94
- - `midnight-compare-approaches` — Compare implementation approaches
95
- - `midnight-debug-contract` — Debug issues
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
 
@@ -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 queryValidation = validateQuery(input.query);
49
- if (!queryValidation.isValid) {
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 limitValidation = validateNumber(input.limit, {
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 = searchCache.get(cacheKey);
70
- if (cached) {
71
- logger.debug("Search cache hit", { cacheKey });
139
+ const cached = checkSearchCache(cacheKey);
140
+ if (cached)
72
141
  return cached;
73
- }
74
- // Use hosted API if in hosted mode
75
- if (isHostedMode()) {
76
- try {
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
- // Cache the response
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 queryValidation = validateQuery(input.query);
127
- if (!queryValidation.isValid) {
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 limitValidation = validateNumber(input.limit, {
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 = searchCache.get(cacheKey);
148
- if (cached) {
149
- logger.debug("Search cache hit", { cacheKey });
185
+ const cached = checkSearchCache(cacheKey);
186
+ if (cached)
150
187
  return cached;
151
- }
152
- // Use hosted API if in hosted mode
153
- if (isHostedMode()) {
154
- try {
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
- searchCache.set(cacheKey, response);
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 queryValidation = validateQuery(input.query);
209
- if (!queryValidation.isValid) {
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 limitValidation = validateNumber(input.limit, {
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 = searchCache.get(cacheKey);
230
- if (cached) {
231
- logger.debug("Search cache hit", { cacheKey });
236
+ const cached = checkSearchCache(cacheKey);
237
+ if (cached)
232
238
  return cached;
233
- }
234
- // Use hosted API if in hosted mode
235
- if (isHostedMode()) {
236
- try {
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
- searchCache.set(cacheKey, response);
281
- return response;
267
+ return finalizeResponse(response, cacheKey, warnings);
282
268
  }
283
269
  // Tool definitions for MCP
284
270
  export const searchTools = [
@@ -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}: ${message}`, "UNKNOWN_ERROR", "If this problem persists, please report it at https://github.com/Olanetsoft/midnight-mcp/issues");
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
@@ -22,6 +22,7 @@ export interface HostedSearchResponse {
22
22
  query: string;
23
23
  category?: string;
24
24
  warnings?: string[];
25
+ lastIndexed?: string | null;
25
26
  }
26
27
  export interface HostedSearchFilter {
27
28
  language?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "midnight-mcp",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
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
  },