midnight-mcp 0.0.4 → 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 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,29 +23,75 @@ Add to your `claude_desktop_config.json`:
17
23
  }
18
24
  ```
19
25
 
20
- Restart Claude Desktop. You can now use analysis tools, prompts, and access resources.
26
+ <details>
27
+ <summary><strong>📍 Config file locations</strong></summary>
21
28
 
22
- > **Note:** Search features won't work well without the full setup below.
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`
23
32
 
24
- ---
33
+ </details>
25
34
 
26
- ## Full Setup (for search)
35
+ ### Cursor
27
36
 
28
- To enable semantic search across Midnight contracts and docs:
37
+ Add to Cursor's MCP settings (Settings MCP → Add Server):
29
38
 
30
- ### 1. Start ChromaDB
39
+ ```json
40
+ {
41
+ "mcpServers": {
42
+ "midnight": {
43
+ "command": "npx",
44
+ "args": ["-y", "midnight-mcp"]
45
+ }
46
+ }
47
+ }
48
+ ```
31
49
 
32
- ChromaDB is a local vector database—no account needed, just Docker:
50
+ Or add to `.cursor/mcp.json` in your project:
33
51
 
34
- ```bash
35
- docker run -d -p 8000:8000 chromadb/chroma
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "midnight": {
56
+ "command": "npx",
57
+ "args": ["-y", "midnight-mcp"]
58
+ }
59
+ }
60
+ }
36
61
  ```
37
62
 
38
- ### 2. Get an OpenAI API key
63
+ ### Windsurf
39
64
 
40
- Needed for generating embeddings. Get one at [platform.openai.com/api-keys](https://platform.openai.com/api-keys).
65
+ Add to `~/.codeium/windsurf/mcp_config.json`:
41
66
 
42
- ### 3. Update your config
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.
81
+
82
+ ---
83
+
84
+ ## How It Works
85
+
86
+ By default, the MCP uses a **hosted API** for semantic search:
87
+
88
+ - ✅ **Zero configuration** — just install and use
89
+ - ✅ **Semantic search** works immediately
90
+ - ✅ **No API keys** needed
91
+
92
+ ### Local Mode (Optional)
93
+
94
+ Run everything locally for privacy or offline use:
43
95
 
44
96
  ```json
45
97
  {
@@ -48,6 +100,7 @@ Needed for generating embeddings. Get one at [platform.openai.com/api-keys](http
48
100
  "command": "npx",
49
101
  "args": ["-y", "midnight-mcp"],
50
102
  "env": {
103
+ "MIDNIGHT_LOCAL": "true",
51
104
  "OPENAI_API_KEY": "sk-...",
52
105
  "CHROMA_URL": "http://localhost:8000"
53
106
  }
@@ -56,15 +109,17 @@ Needed for generating embeddings. Get one at [platform.openai.com/api-keys](http
56
109
  }
57
110
  ```
58
111
 
59
- ### Optional: GitHub token
112
+ Local mode requires ChromaDB (`docker run -d -p 8000:8000 chromadb/chroma`) and an OpenAI API key.
113
+
114
+ ### GitHub Token (Optional)
60
115
 
61
- Add `"GITHUB_TOKEN": "ghp_..."` to increase API rate limits from 60 to 5000 requests/hour.
116
+ Add `"GITHUB_TOKEN": "ghp_..."` for higher GitHub API rate limits (60 5000 requests/hour).
62
117
 
63
118
  ---
64
119
 
65
- ## What's Included
120
+ ## Features
66
121
 
67
- ### Tools (14 total)
122
+ ### Tools (16)
68
123
 
69
124
  | Tool | Description |
70
125
  | --------------------------------- | --------------------------------------- |
@@ -82,39 +137,28 @@ Add `"GITHUB_TOKEN": "ghp_..."` to increase API rate limits from 60 to 5000 requ
82
137
  | `midnight-get-file-at-version` | Get file at specific version |
83
138
  | `midnight-compare-syntax` | Compare files between versions |
84
139
  | `midnight-get-latest-syntax` | Latest syntax reference |
140
+ | `midnight-health-check` | Check server health status |
141
+ | `midnight-get-status` | Get rate limits and cache stats |
85
142
 
86
- ### Resources (20 total)
143
+ ### Resources (20)
87
144
 
88
- - `midnight://docs/*` — Documentation (Compact reference, SDK API, ZK concepts, OpenZeppelin patterns)
145
+ - `midnight://docs/*` — Documentation (Compact reference, SDK API, ZK concepts)
89
146
  - `midnight://code/*` — Examples, patterns, and templates
90
147
  - `midnight://schema/*` — AST, transaction, and proof schemas
91
148
 
92
- ### Prompts
149
+ ### Prompts (5)
93
150
 
94
- - `midnight-create-contract` — Create new contracts
95
- - `midnight-review-contract` — Security review
96
- - `midnight-explain-concept` — Learn Midnight concepts
97
- - `midnight-debug-contract` — Debug issues
98
-
99
- ---
100
-
101
- ## How Environment Variables Work
102
-
103
- The npm package contains no secrets. **You provide your own credentials** via the `env` block in your config:
104
-
105
- | Variable | Required | Without It | With It |
106
- | ---------------- | -------- | -------------------------------------- | -------------------- |
107
- | `GITHUB_TOKEN` | No | 60 API calls/hour, may hit rate limits | 5,000 calls/hour |
108
- | `OPENAI_API_KEY` | No | Keyword search only (no embeddings) | Semantic search |
109
- | `CHROMA_URL` | No | In-memory search, no persistence | Persistent vector DB |
110
-
111
- Your tokens stay on your machine and are only used to access services on your behalf.
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
112
156
 
113
157
  ---
114
158
 
115
159
  ## Developer Setup
116
160
 
117
- For contributors who want to modify or extend the MCP server.
161
+ For contributors:
118
162
 
119
163
  ```bash
120
164
  git clone https://github.com/Olanetsoft/midnight-mcp.git
@@ -124,26 +168,23 @@ npm run build
124
168
  npm test
125
169
  ```
126
170
 
127
- ### Index Midnight repos (for search)
171
+ ### Testing with Local API
172
+
173
+ To test against a local API server instead of production:
128
174
 
129
175
  ```bash
130
- docker run -d -p 8000:8000 chromadb/chroma
131
- npm run index
176
+ # Terminal 1: Start local API
177
+ cd api
178
+ npm install
179
+ npm run dev # Starts at http://localhost:8787
180
+
181
+ # Terminal 2: Run MCP with local API
182
+ MIDNIGHT_API_URL=http://localhost:8787 npm start
132
183
  ```
133
184
 
134
- ### Project Structure
185
+ ### API Backend
135
186
 
136
- ```
137
- src/
138
- ├── index.ts # Entry point
139
- ├── server.ts # MCP server handlers
140
- ├── tools/ # Search, analysis, repository tools
141
- ├── resources/ # Docs, code, schema providers
142
- ├── prompts/ # Prompt templates
143
- ├── pipeline/ # GitHub sync & parsing
144
- ├── db/ # ChromaDB integration
145
- └── utils/ # Config & logging
146
- ```
187
+ The hosted API runs on Cloudflare Workers + Vectorize. See [api/README.md](./api/README.md) for deployment and development instructions.
147
188
 
148
189
  ## License
149
190
 
@@ -50,11 +50,11 @@ export declare const SearchDocsInputSchema: z.ZodObject<{
50
50
  }, "strip", z.ZodTypeAny, {
51
51
  query: string;
52
52
  limit: number;
53
- category: "guides" | "api" | "concepts" | "all";
53
+ category: "all" | "guides" | "api" | "concepts";
54
54
  }, {
55
55
  query: string;
56
56
  limit?: number | undefined;
57
- category?: "guides" | "api" | "concepts" | "all" | undefined;
57
+ category?: "all" | "guides" | "api" | "concepts" | undefined;
58
58
  }>;
59
59
  export type SearchCompactInput = z.infer<typeof SearchCompactInputSchema>;
60
60
  export type SearchTypeScriptInput = z.infer<typeof SearchTypeScriptInputSchema>;
@@ -1,6 +1,86 @@
1
1
  import { z } from "zod";
2
2
  import { vectorStore } from "../db/index.js";
3
- import { logger, validateQuery, validateNumber, searchCache, createCacheKey, } from "../utils/index.js";
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,33 +124,26 @@ 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
- originalQuery: input.query,
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
- }
142
+ // Try hosted API first
143
+ const hostedResult = await tryHostedSearch("compact", () => searchCompactHosted(sanitizedQuery, limit), cacheKey, warnings);
144
+ if (hostedResult)
145
+ return hostedResult.result;
146
+ // Local search (fallback or when in local mode)
74
147
  const filter = {
75
148
  language: "compact",
76
149
  ...input.filter,
@@ -90,42 +163,33 @@ export async function searchCompact(input) {
90
163
  })),
91
164
  totalResults: results.length,
92
165
  query: sanitizedQuery,
93
- ...(queryValidation.warnings.length > 0 && {
94
- warnings: queryValidation.warnings,
95
- }),
96
166
  };
97
- // Cache the response
98
- searchCache.set(cacheKey, response);
99
- return response;
167
+ return finalizeResponse(response, cacheKey, warnings);
100
168
  }
101
169
  /**
102
170
  * Search TypeScript SDK code, types, and API implementations
103
171
  */
104
172
  export async function searchTypeScript(input) {
105
- // Validate input
106
- const queryValidation = validateQuery(input.query);
107
- if (!queryValidation.isValid) {
108
- return {
109
- error: "Invalid query",
110
- details: queryValidation.errors,
111
- suggestion: "Provide a valid search query with at least 2 characters",
112
- };
173
+ // Validate input using common helper
174
+ const validation = validateSearchInput(input.query, input.limit);
175
+ if (!validation.success) {
176
+ return validation.error;
113
177
  }
114
- const limitValidation = validateNumber(input.limit, {
115
- min: 1,
116
- max: 50,
117
- defaultValue: 10,
178
+ const { sanitizedQuery, limit, warnings } = validation.context;
179
+ logger.debug("Searching TypeScript code", {
180
+ query: sanitizedQuery,
181
+ mode: isHostedMode() ? "hosted" : "local",
118
182
  });
119
- const sanitizedQuery = queryValidation.sanitized;
120
- const limit = limitValidation.value;
121
- logger.debug("Searching TypeScript code", { query: sanitizedQuery });
122
183
  // Check cache
123
184
  const cacheKey = createCacheKey("typescript", sanitizedQuery, limit, input.includeTypes, input.includeExamples);
124
- const cached = searchCache.get(cacheKey);
125
- if (cached) {
126
- logger.debug("Search cache hit", { cacheKey });
185
+ const cached = checkSearchCache(cacheKey);
186
+ if (cached)
127
187
  return cached;
128
- }
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;
192
+ // Local search (fallback or when in local mode)
129
193
  const filter = {
130
194
  language: "typescript",
131
195
  };
@@ -150,41 +214,33 @@ export async function searchTypeScript(input) {
150
214
  })),
151
215
  totalResults: filteredResults.length,
152
216
  query: sanitizedQuery,
153
- ...(queryValidation.warnings.length > 0 && {
154
- warnings: queryValidation.warnings,
155
- }),
156
217
  };
157
- searchCache.set(cacheKey, response);
158
- return response;
218
+ return finalizeResponse(response, cacheKey, warnings);
159
219
  }
160
220
  /**
161
221
  * Full-text search across official Midnight documentation
162
222
  */
163
223
  export async function searchDocs(input) {
164
- // Validate input
165
- const queryValidation = validateQuery(input.query);
166
- if (!queryValidation.isValid) {
167
- return {
168
- error: "Invalid query",
169
- details: queryValidation.errors,
170
- suggestion: "Provide a valid search query with at least 2 characters",
171
- };
224
+ // Validate input using common helper
225
+ const validation = validateSearchInput(input.query, input.limit);
226
+ if (!validation.success) {
227
+ return validation.error;
172
228
  }
173
- const limitValidation = validateNumber(input.limit, {
174
- min: 1,
175
- max: 50,
176
- defaultValue: 10,
229
+ const { sanitizedQuery, limit, warnings } = validation.context;
230
+ logger.debug("Searching documentation", {
231
+ query: sanitizedQuery,
232
+ mode: isHostedMode() ? "hosted" : "local",
177
233
  });
178
- const sanitizedQuery = queryValidation.sanitized;
179
- const limit = limitValidation.value;
180
- logger.debug("Searching documentation", { query: sanitizedQuery });
181
234
  // Check cache
182
235
  const cacheKey = createCacheKey("docs", sanitizedQuery, limit, input.category);
183
- const cached = searchCache.get(cacheKey);
184
- if (cached) {
185
- logger.debug("Search cache hit", { cacheKey });
236
+ const cached = checkSearchCache(cacheKey);
237
+ if (cached)
186
238
  return cached;
187
- }
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;
243
+ // Local search (fallback or when in local mode)
188
244
  const filter = {
189
245
  language: "markdown",
190
246
  };
@@ -207,12 +263,8 @@ export async function searchDocs(input) {
207
263
  totalResults: results.length,
208
264
  query: sanitizedQuery,
209
265
  category: input.category,
210
- ...(queryValidation.warnings.length > 0 && {
211
- warnings: queryValidation.warnings,
212
- }),
213
266
  };
214
- searchCache.set(cacheKey, response);
215
- return response;
267
+ return finalizeResponse(response, cacheKey, warnings);
216
268
  }
217
269
  // Tool definitions for MCP
218
270
  export const searchTools = [
@@ -1,10 +1,9 @@
1
1
  import { z } from "zod";
2
2
  declare const ConfigSchema: z.ZodObject<{
3
+ mode: z.ZodDefault<z.ZodEnum<["hosted", "local"]>>;
4
+ hostedApiUrl: z.ZodDefault<z.ZodString>;
3
5
  githubToken: z.ZodOptional<z.ZodString>;
4
6
  chromaUrl: z.ZodDefault<z.ZodString>;
5
- qdrantUrl: z.ZodOptional<z.ZodString>;
6
- pineconeApiKey: z.ZodOptional<z.ZodString>;
7
- pineconeIndex: z.ZodOptional<z.ZodString>;
8
7
  openaiApiKey: z.ZodOptional<z.ZodString>;
9
8
  embeddingModel: z.ZodDefault<z.ZodString>;
10
9
  logLevel: z.ZodDefault<z.ZodEnum<["debug", "info", "warn", "error"]>>;
@@ -13,6 +12,8 @@ declare const ConfigSchema: z.ZodObject<{
13
12
  dataDir: z.ZodDefault<z.ZodString>;
14
13
  cacheDir: z.ZodDefault<z.ZodString>;
15
14
  }, "strip", z.ZodTypeAny, {
15
+ mode: "hosted" | "local";
16
+ hostedApiUrl: string;
16
17
  chromaUrl: string;
17
18
  embeddingModel: string;
18
19
  logLevel: "debug" | "info" | "warn" | "error";
@@ -21,16 +22,12 @@ declare const ConfigSchema: z.ZodObject<{
21
22
  dataDir: string;
22
23
  cacheDir: string;
23
24
  githubToken?: string | undefined;
24
- qdrantUrl?: string | undefined;
25
- pineconeApiKey?: string | undefined;
26
- pineconeIndex?: string | undefined;
27
25
  openaiApiKey?: string | undefined;
28
26
  }, {
27
+ mode?: "hosted" | "local" | undefined;
28
+ hostedApiUrl?: string | undefined;
29
29
  githubToken?: string | undefined;
30
30
  chromaUrl?: string | undefined;
31
- qdrantUrl?: string | undefined;
32
- pineconeApiKey?: string | undefined;
33
- pineconeIndex?: string | undefined;
34
31
  openaiApiKey?: string | undefined;
35
32
  embeddingModel?: string | undefined;
36
33
  logLevel?: "debug" | "info" | "warn" | "error" | undefined;
@@ -41,6 +38,8 @@ declare const ConfigSchema: z.ZodObject<{
41
38
  }>;
42
39
  export type Config = z.infer<typeof ConfigSchema>;
43
40
  export declare const config: {
41
+ mode: "hosted" | "local";
42
+ hostedApiUrl: string;
44
43
  chromaUrl: string;
45
44
  embeddingModel: string;
46
45
  logLevel: "debug" | "info" | "warn" | "error";
@@ -49,11 +48,16 @@ export declare const config: {
49
48
  dataDir: string;
50
49
  cacheDir: string;
51
50
  githubToken?: string | undefined;
52
- qdrantUrl?: string | undefined;
53
- pineconeApiKey?: string | undefined;
54
- pineconeIndex?: string | undefined;
55
51
  openaiApiKey?: string | undefined;
56
52
  };
53
+ /**
54
+ * Check if running in hosted mode (default)
55
+ */
56
+ export declare function isHostedMode(): boolean;
57
+ /**
58
+ * Check if running in local mode
59
+ */
60
+ export declare function isLocalMode(): boolean;
57
61
  export interface RepositoryConfig {
58
62
  owner: string;
59
63
  repo: string;
@@ -2,14 +2,17 @@ import { z } from "zod";
2
2
  import dotenv from "dotenv";
3
3
  dotenv.config();
4
4
  const ConfigSchema = z.object({
5
+ // Mode: 'hosted' (default) or 'local'
6
+ mode: z.enum(["hosted", "local"]).default("hosted"),
7
+ // Hosted API URL (used when mode is 'hosted')
8
+ hostedApiUrl: z
9
+ .string()
10
+ .default("https://midnight-mcp-api.midnightmcp.workers.dev"),
5
11
  // GitHub
6
12
  githubToken: z.string().optional(),
7
- // Vector Database
13
+ // Vector Database (only needed for local mode)
8
14
  chromaUrl: z.string().default("http://localhost:8000"),
9
- qdrantUrl: z.string().optional(),
10
- pineconeApiKey: z.string().optional(),
11
- pineconeIndex: z.string().optional(),
12
- // Embeddings
15
+ // Embeddings (only needed for local mode)
13
16
  openaiApiKey: z.string().optional(),
14
17
  embeddingModel: z.string().default("text-embedding-3-small"),
15
18
  // Server
@@ -21,12 +24,14 @@ const ConfigSchema = z.object({
21
24
  cacheDir: z.string().default("./cache"),
22
25
  });
23
26
  function loadConfig() {
27
+ // Determine mode: local if MIDNIGHT_LOCAL=true or if OPENAI_API_KEY is set
28
+ const isLocalMode = process.env.MIDNIGHT_LOCAL === "true" ||
29
+ (process.env.OPENAI_API_KEY && process.env.CHROMA_URL);
24
30
  const rawConfig = {
31
+ mode: isLocalMode ? "local" : "hosted",
32
+ hostedApiUrl: process.env.MIDNIGHT_API_URL,
25
33
  githubToken: process.env.GITHUB_TOKEN,
26
34
  chromaUrl: process.env.CHROMA_URL,
27
- qdrantUrl: process.env.QDRANT_URL,
28
- pineconeApiKey: process.env.PINECONE_API_KEY,
29
- pineconeIndex: process.env.PINECONE_INDEX,
30
35
  openaiApiKey: process.env.OPENAI_API_KEY,
31
36
  embeddingModel: process.env.EMBEDDING_MODEL,
32
37
  logLevel: process.env.LOG_LEVEL,
@@ -42,6 +47,18 @@ function loadConfig() {
42
47
  return ConfigSchema.parse(cleanConfig);
43
48
  }
44
49
  export const config = loadConfig();
50
+ /**
51
+ * Check if running in hosted mode (default)
52
+ */
53
+ export function isHostedMode() {
54
+ return config.mode === "hosted";
55
+ }
56
+ /**
57
+ * Check if running in local mode
58
+ */
59
+ export function isLocalMode() {
60
+ return config.mode === "local";
61
+ }
45
62
  export const DEFAULT_REPOSITORIES = [
46
63
  // Core Language & SDK
47
64
  {
@@ -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
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Client for the hosted Midnight MCP API
3
+ * Used when running in hosted mode (default)
4
+ */
5
+ export interface HostedSearchResult {
6
+ code?: string;
7
+ content?: string;
8
+ relevanceScore: number;
9
+ source: {
10
+ repository: string;
11
+ filePath: string;
12
+ lines?: string;
13
+ section?: string;
14
+ };
15
+ codeType?: string;
16
+ name?: string;
17
+ isExported?: boolean;
18
+ }
19
+ export interface HostedSearchResponse {
20
+ results: HostedSearchResult[];
21
+ totalResults: number;
22
+ query: string;
23
+ category?: string;
24
+ warnings?: string[];
25
+ }
26
+ export interface HostedSearchFilter {
27
+ language?: string;
28
+ repository?: string;
29
+ }
30
+ /**
31
+ * Search Compact code via hosted API
32
+ */
33
+ export declare function searchCompactHosted(query: string, limit?: number): Promise<HostedSearchResponse>;
34
+ /**
35
+ * Search TypeScript code via hosted API
36
+ */
37
+ export declare function searchTypeScriptHosted(query: string, limit?: number, includeTypes?: boolean): Promise<HostedSearchResponse>;
38
+ /**
39
+ * Search documentation via hosted API
40
+ */
41
+ export declare function searchDocsHosted(query: string, limit?: number, category?: string): Promise<HostedSearchResponse>;
42
+ /**
43
+ * Generic search via hosted API
44
+ */
45
+ export declare function searchHosted(query: string, limit?: number, filter?: HostedSearchFilter): Promise<HostedSearchResponse>;
46
+ /**
47
+ * Check if the hosted API is available
48
+ */
49
+ export declare function checkHostedApiHealth(): Promise<{
50
+ available: boolean;
51
+ documentsIndexed?: number;
52
+ error?: string;
53
+ }>;
54
+ /**
55
+ * Get hosted API stats
56
+ */
57
+ export declare function getHostedApiStats(): Promise<{
58
+ documentsIndexed: number;
59
+ repositories: number;
60
+ }>;
61
+ //# sourceMappingURL=hosted-api.d.ts.map
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Client for the hosted Midnight MCP API
3
+ * Used when running in hosted mode (default)
4
+ */
5
+ import { config, logger } from "./index.js";
6
+ const API_TIMEOUT = 10000; // 10 seconds
7
+ /**
8
+ * Make a request to the hosted API
9
+ */
10
+ async function apiRequest(endpoint, options = {}) {
11
+ const url = `${config.hostedApiUrl}${endpoint}`;
12
+ const controller = new AbortController();
13
+ const timeout = setTimeout(() => controller.abort(), API_TIMEOUT);
14
+ try {
15
+ const response = await fetch(url, {
16
+ ...options,
17
+ signal: controller.signal,
18
+ headers: {
19
+ "Content-Type": "application/json",
20
+ "User-Agent": "midnight-mcp",
21
+ ...options.headers,
22
+ },
23
+ });
24
+ if (!response.ok) {
25
+ const errorData = (await response
26
+ .json()
27
+ .catch(() => ({ error: "Unknown error" })));
28
+ throw new Error(errorData.error || `API error: ${response.status}`);
29
+ }
30
+ return (await response.json());
31
+ }
32
+ catch (error) {
33
+ if (error instanceof Error && error.name === "AbortError") {
34
+ throw new Error("API request timed out. The hosted service may be unavailable.");
35
+ }
36
+ throw error;
37
+ }
38
+ finally {
39
+ clearTimeout(timeout);
40
+ }
41
+ }
42
+ /**
43
+ * Search Compact code via hosted API
44
+ */
45
+ export async function searchCompactHosted(query, limit = 10) {
46
+ logger.debug("Searching Compact code via hosted API", { query });
47
+ return apiRequest("/v1/search/compact", {
48
+ method: "POST",
49
+ body: JSON.stringify({ query, limit }),
50
+ });
51
+ }
52
+ /**
53
+ * Search TypeScript code via hosted API
54
+ */
55
+ export async function searchTypeScriptHosted(query, limit = 10, includeTypes = true) {
56
+ logger.debug("Searching TypeScript code via hosted API", { query });
57
+ return apiRequest("/v1/search/typescript", {
58
+ method: "POST",
59
+ body: JSON.stringify({ query, limit, includeTypes }),
60
+ });
61
+ }
62
+ /**
63
+ * Search documentation via hosted API
64
+ */
65
+ export async function searchDocsHosted(query, limit = 10, category = "all") {
66
+ logger.debug("Searching documentation via hosted API", { query });
67
+ return apiRequest("/v1/search/docs", {
68
+ method: "POST",
69
+ body: JSON.stringify({ query, limit, category }),
70
+ });
71
+ }
72
+ /**
73
+ * Generic search via hosted API
74
+ */
75
+ export async function searchHosted(query, limit = 10, filter) {
76
+ logger.debug("Searching via hosted API", { query, filter });
77
+ return apiRequest("/v1/search", {
78
+ method: "POST",
79
+ body: JSON.stringify({ query, limit, filter }),
80
+ });
81
+ }
82
+ /**
83
+ * Check if the hosted API is available
84
+ */
85
+ export async function checkHostedApiHealth() {
86
+ try {
87
+ const response = await apiRequest("/health");
88
+ return {
89
+ available: response.status === "healthy",
90
+ documentsIndexed: response.vectorStore?.documentsIndexed,
91
+ };
92
+ }
93
+ catch (error) {
94
+ return {
95
+ available: false,
96
+ error: error instanceof Error ? error.message : String(error),
97
+ };
98
+ }
99
+ }
100
+ /**
101
+ * Get hosted API stats
102
+ */
103
+ export async function getHostedApiStats() {
104
+ return apiRequest("/v1/stats");
105
+ }
106
+ //# sourceMappingURL=hosted-api.js.map
@@ -1,4 +1,4 @@
1
- export { config } from "./config.js";
1
+ export { config, isHostedMode, isLocalMode } from "./config.js";
2
2
  export type { Config, RepositoryConfig } from "./config.js";
3
3
  export { DEFAULT_REPOSITORIES } from "./config.js";
4
4
  export { logger } from "./logger.js";
@@ -11,4 +11,6 @@ export { updateRateLimitFromHeaders, updateRateLimit, getRateLimitStatus, should
11
11
  export type { RateLimitInfo, RateLimitStatus } from "./rate-limit.js";
12
12
  export { Cache, createCacheKey, searchCache, fileCache, metadataCache, pruneAllCaches, } from "./cache.js";
13
13
  export type { CacheOptions, CacheEntry, CacheStats } from "./cache.js";
14
+ export { searchCompactHosted, searchTypeScriptHosted, searchDocsHosted, searchHosted, checkHostedApiHealth, getHostedApiStats, } from "./hosted-api.js";
15
+ export type { HostedSearchResult, HostedSearchResponse, HostedSearchFilter, } from "./hosted-api.js";
14
16
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,4 @@
1
- export { config } from "./config.js";
1
+ export { config, isHostedMode, isLocalMode } from "./config.js";
2
2
  export { DEFAULT_REPOSITORIES } from "./config.js";
3
3
  export { logger } from "./logger.js";
4
4
  export { MCPError, ErrorCodes, createUserError, formatErrorResponse, withErrorHandling, } from "./errors.js";
@@ -10,4 +10,6 @@ export { getHealthStatus, getQuickHealthStatus } from "./health.js";
10
10
  export { updateRateLimitFromHeaders, updateRateLimit, getRateLimitStatus, shouldProceedWithRequest, getTimeUntilReset, formatRateLimitStatus, decrementRemaining, } from "./rate-limit.js";
11
11
  // Caching utilities
12
12
  export { Cache, createCacheKey, searchCache, fileCache, metadataCache, pruneAllCaches, } from "./cache.js";
13
+ // Hosted API client
14
+ export { searchCompactHosted, searchTypeScriptHosted, searchDocsHosted, searchHosted, checkHostedApiHealth, getHostedApiStats, } from "./hosted-api.js";
13
15
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "midnight-mcp",
3
- "version": "0.0.4",
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",
@@ -11,10 +11,8 @@
11
11
  "build": "tsc",
12
12
  "start": "node dist/index.js",
13
13
  "dev": "tsx watch src/index.ts",
14
- "index": "tsx src/scripts/index-repos.ts",
15
14
  "test": "vitest",
16
15
  "test:coverage": "vitest --coverage",
17
- "lint": "eslint src/**/*.ts",
18
16
  "format": "prettier --write src/**/*.ts",
19
17
  "prepublishOnly": "npm run build"
20
18
  },