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 +94 -53
- package/dist/tools/search.d.ts +2 -2
- package/dist/tools/search.js +127 -75
- package/dist/utils/config.d.ts +16 -12
- package/dist/utils/config.js +25 -8
- package/dist/utils/errors.js +2 -2
- package/dist/utils/hosted-api.d.ts +61 -0
- package/dist/utils/hosted-api.js +106 -0
- package/dist/utils/index.d.ts +3 -1
- package/dist/utils/index.js +3 -1
- package/package.json +1 -3
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,29 +23,75 @@ Add to your `claude_desktop_config.json`:
|
|
|
17
23
|
}
|
|
18
24
|
```
|
|
19
25
|
|
|
20
|
-
|
|
26
|
+
<details>
|
|
27
|
+
<summary><strong>📍 Config file locations</strong></summary>
|
|
21
28
|
|
|
22
|
-
|
|
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
|
-
|
|
35
|
+
### Cursor
|
|
27
36
|
|
|
28
|
-
|
|
37
|
+
Add to Cursor's MCP settings (Settings → MCP → Add Server):
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"midnight": {
|
|
43
|
+
"command": "npx",
|
|
44
|
+
"args": ["-y", "midnight-mcp"]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
31
49
|
|
|
32
|
-
|
|
50
|
+
Or add to `.cursor/mcp.json` in your project:
|
|
33
51
|
|
|
34
|
-
```
|
|
35
|
-
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"mcpServers": {
|
|
55
|
+
"midnight": {
|
|
56
|
+
"command": "npx",
|
|
57
|
+
"args": ["-y", "midnight-mcp"]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
36
61
|
```
|
|
37
62
|
|
|
38
|
-
###
|
|
63
|
+
### Windsurf
|
|
39
64
|
|
|
40
|
-
|
|
65
|
+
Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
41
66
|
|
|
42
|
-
|
|
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
|
-
|
|
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_..."`
|
|
116
|
+
Add `"GITHUB_TOKEN": "ghp_..."` for higher GitHub API rate limits (60 → 5000 requests/hour).
|
|
62
117
|
|
|
63
118
|
---
|
|
64
119
|
|
|
65
|
-
##
|
|
120
|
+
## Features
|
|
66
121
|
|
|
67
|
-
### Tools (
|
|
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
|
|
143
|
+
### Resources (20)
|
|
87
144
|
|
|
88
|
-
- `midnight://docs/*` — Documentation (Compact reference, SDK API, ZK concepts
|
|
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
|
|
95
|
-
- `midnight
|
|
96
|
-
- `midnight
|
|
97
|
-
- `midnight-
|
|
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
|
|
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
|
-
###
|
|
171
|
+
### Testing with Local API
|
|
172
|
+
|
|
173
|
+
To test against a local API server instead of production:
|
|
128
174
|
|
|
129
175
|
```bash
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
###
|
|
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
|
|
package/dist/tools/search.d.ts
CHANGED
|
@@ -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: "
|
|
53
|
+
category: "all" | "guides" | "api" | "concepts";
|
|
54
54
|
}, {
|
|
55
55
|
query: string;
|
|
56
56
|
limit?: number | undefined;
|
|
57
|
-
category?: "
|
|
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>;
|
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
|
-
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
107
|
-
if (!
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
166
|
-
if (!
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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 =
|
|
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
|
-
|
|
215
|
-
return response;
|
|
267
|
+
return finalizeResponse(response, cacheKey, warnings);
|
|
216
268
|
}
|
|
217
269
|
// Tool definitions for MCP
|
|
218
270
|
export const searchTools = [
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/config.js
CHANGED
|
@@ -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
|
-
|
|
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
|
{
|
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
|
|
@@ -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
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -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
|
package/dist/utils/index.js
CHANGED
|
@@ -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.
|
|
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
|
},
|