midnight-mcp 0.0.2 → 0.0.5

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.
@@ -1,9 +1,11 @@
1
- export { searchTools, searchCompact, searchTypeScript, searchDocs } from "./search.js";
2
- export type { SearchCompactInput, SearchTypeScriptInput, SearchDocsInput } from "./search.js";
1
+ export { searchTools, searchCompact, searchTypeScript, searchDocs, } from "./search.js";
2
+ export type { SearchCompactInput, SearchTypeScriptInput, SearchDocsInput, } from "./search.js";
3
3
  export { analyzeTools, analyzeContract, explainCircuit } from "./analyze.js";
4
4
  export type { AnalyzeContractInput, ExplainCircuitInput } from "./analyze.js";
5
- export { repositoryTools, getFile, listExamples, getLatestUpdates } from "./repository.js";
6
- export type { GetFileInput, ListExamplesInput, GetLatestUpdatesInput } from "./repository.js";
5
+ export { repositoryTools, getFile, listExamples, getLatestUpdates, } from "./repository.js";
6
+ export type { GetFileInput, ListExamplesInput, GetLatestUpdatesInput, } from "./repository.js";
7
+ export { healthTools, healthCheck, getStatus } from "./health.js";
8
+ export type { HealthCheckInput, GetStatusInput } from "./health.js";
7
9
  export declare const allTools: ({
8
10
  name: string;
9
11
  description: string;
@@ -388,5 +390,29 @@ export declare const allTools: ({
388
390
  required: never[];
389
391
  };
390
392
  handler: typeof import("./repository.js").getLatestSyntax;
393
+ } | {
394
+ name: string;
395
+ description: string;
396
+ inputSchema: {
397
+ type: "object";
398
+ properties: {
399
+ detailed: {
400
+ type: string;
401
+ description: string;
402
+ default: boolean;
403
+ };
404
+ };
405
+ };
406
+ handler: typeof import("./health.js").healthCheck;
407
+ } | {
408
+ name: string;
409
+ description: string;
410
+ inputSchema: {
411
+ type: "object";
412
+ properties: {
413
+ detailed?: undefined;
414
+ };
415
+ };
416
+ handler: typeof import("./health.js").getStatus;
391
417
  })[];
392
418
  //# sourceMappingURL=index.d.ts.map
@@ -1,9 +1,16 @@
1
- export { searchTools, searchCompact, searchTypeScript, searchDocs } from "./search.js";
1
+ export { searchTools, searchCompact, searchTypeScript, searchDocs, } from "./search.js";
2
2
  export { analyzeTools, analyzeContract, explainCircuit } from "./analyze.js";
3
- export { repositoryTools, getFile, listExamples, getLatestUpdates } from "./repository.js";
3
+ export { repositoryTools, getFile, listExamples, getLatestUpdates, } from "./repository.js";
4
+ export { healthTools, healthCheck, getStatus } from "./health.js";
4
5
  // Combined tool list for MCP server
5
6
  import { searchTools } from "./search.js";
6
7
  import { analyzeTools } from "./analyze.js";
7
8
  import { repositoryTools } from "./repository.js";
8
- export const allTools = [...searchTools, ...analyzeTools, ...repositoryTools];
9
+ import { healthTools } from "./health.js";
10
+ export const allTools = [
11
+ ...searchTools,
12
+ ...analyzeTools,
13
+ ...repositoryTools,
14
+ ...healthTools,
15
+ ];
9
16
  //# sourceMappingURL=index.js.map
@@ -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>;
@@ -62,57 +62,15 @@ export type SearchDocsInput = z.infer<typeof SearchDocsInputSchema>;
62
62
  /**
63
63
  * Search Compact smart contract code and patterns
64
64
  */
65
- export declare function searchCompact(input: SearchCompactInput): Promise<{
66
- results: {
67
- code: string;
68
- relevanceScore: number;
69
- source: {
70
- repository: string;
71
- filePath: string;
72
- lines: string;
73
- };
74
- codeType: string;
75
- name: string;
76
- }[];
77
- totalResults: number;
78
- query: string;
79
- }>;
65
+ export declare function searchCompact(input: SearchCompactInput): Promise<{}>;
80
66
  /**
81
67
  * Search TypeScript SDK code, types, and API implementations
82
68
  */
83
- export declare function searchTypeScript(input: SearchTypeScriptInput): Promise<{
84
- results: {
85
- code: string;
86
- relevanceScore: number;
87
- source: {
88
- repository: string;
89
- filePath: string;
90
- lines: string;
91
- };
92
- codeType: string;
93
- name: string;
94
- isExported: boolean;
95
- }[];
96
- totalResults: number;
97
- query: string;
98
- }>;
69
+ export declare function searchTypeScript(input: SearchTypeScriptInput): Promise<{}>;
99
70
  /**
100
71
  * Full-text search across official Midnight documentation
101
72
  */
102
- export declare function searchDocs(input: SearchDocsInput): Promise<{
103
- results: {
104
- content: string;
105
- relevanceScore: number;
106
- source: {
107
- repository: string;
108
- filePath: string;
109
- section: string;
110
- };
111
- }[];
112
- totalResults: number;
113
- query: string;
114
- category: "guides" | "api" | "concepts" | "all";
115
- }>;
73
+ export declare function searchDocs(input: SearchDocsInput): Promise<{}>;
116
74
  export declare const searchTools: ({
117
75
  name: string;
118
76
  description: string;
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { vectorStore } from "../db/index.js";
3
- import { logger } from "../utils/index.js";
3
+ import { logger, validateQuery, validateNumber, searchCache, createCacheKey, isHostedMode, searchCompactHosted, searchTypeScriptHosted, searchDocsHosted, } from "../utils/index.js";
4
4
  // Schema definitions for tool inputs
5
5
  export const SearchCompactInputSchema = z.object({
6
6
  query: z.string().describe("Natural language search query for Compact code"),
@@ -44,13 +44,59 @@ export const SearchDocsInputSchema = z.object({
44
44
  * Search Compact smart contract code and patterns
45
45
  */
46
46
  export async function searchCompact(input) {
47
- logger.debug("Searching Compact code", { query: input.query });
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
+ };
55
+ }
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;
63
+ logger.debug("Searching Compact code", {
64
+ query: sanitizedQuery,
65
+ mode: isHostedMode() ? "hosted" : "local",
66
+ });
67
+ // Check cache first
68
+ 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 });
72
+ 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
+ }
93
+ // Local search (fallback or when in local mode)
48
94
  const filter = {
49
95
  language: "compact",
50
96
  ...input.filter,
51
97
  };
52
- const results = await vectorStore.search(input.query, input.limit, filter);
53
- return {
98
+ const results = await vectorStore.search(sanitizedQuery, limit, filter);
99
+ const response = {
54
100
  results: results.map((r) => ({
55
101
  code: r.content,
56
102
  relevanceScore: r.score,
@@ -63,24 +109,76 @@ export async function searchCompact(input) {
63
109
  name: r.metadata.codeName,
64
110
  })),
65
111
  totalResults: results.length,
66
- query: input.query,
112
+ query: sanitizedQuery,
113
+ ...(queryValidation.warnings.length > 0 && {
114
+ warnings: queryValidation.warnings,
115
+ }),
67
116
  };
117
+ // Cache the response
118
+ searchCache.set(cacheKey, response);
119
+ return response;
68
120
  }
69
121
  /**
70
122
  * Search TypeScript SDK code, types, and API implementations
71
123
  */
72
124
  export async function searchTypeScript(input) {
73
- logger.debug("Searching TypeScript code", { query: input.query });
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
+ };
133
+ }
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;
141
+ logger.debug("Searching TypeScript code", {
142
+ query: sanitizedQuery,
143
+ mode: isHostedMode() ? "hosted" : "local",
144
+ });
145
+ // Check cache
146
+ 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 });
150
+ 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
+ }
171
+ // Local search (fallback or when in local mode)
74
172
  const filter = {
75
173
  language: "typescript",
76
174
  };
77
- const results = await vectorStore.search(input.query, input.limit, filter);
175
+ const results = await vectorStore.search(sanitizedQuery, limit, filter);
78
176
  // Filter based on type preferences
79
177
  let filteredResults = results;
80
178
  if (!input.includeTypes) {
81
179
  filteredResults = results.filter((r) => r.metadata.codeType !== "type" && r.metadata.codeType !== "interface");
82
180
  }
83
- return {
181
+ const response = {
84
182
  results: filteredResults.map((r) => ({
85
183
  code: r.content,
86
184
  relevanceScore: r.score,
@@ -94,14 +192,65 @@ export async function searchTypeScript(input) {
94
192
  isExported: r.metadata.isPublic,
95
193
  })),
96
194
  totalResults: filteredResults.length,
97
- query: input.query,
195
+ query: sanitizedQuery,
196
+ ...(queryValidation.warnings.length > 0 && {
197
+ warnings: queryValidation.warnings,
198
+ }),
98
199
  };
200
+ searchCache.set(cacheKey, response);
201
+ return response;
99
202
  }
100
203
  /**
101
204
  * Full-text search across official Midnight documentation
102
205
  */
103
206
  export async function searchDocs(input) {
104
- logger.debug("Searching documentation", { query: input.query });
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
+ };
215
+ }
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;
223
+ logger.debug("Searching documentation", {
224
+ query: sanitizedQuery,
225
+ mode: isHostedMode() ? "hosted" : "local",
226
+ });
227
+ // Check cache
228
+ const cacheKey = createCacheKey("docs", sanitizedQuery, limit, input.category);
229
+ const cached = searchCache.get(cacheKey);
230
+ if (cached) {
231
+ logger.debug("Search cache hit", { cacheKey });
232
+ 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
+ }
253
+ // Local search (fallback or when in local mode)
105
254
  const filter = {
106
255
  language: "markdown",
107
256
  };
@@ -110,8 +259,8 @@ export async function searchDocs(input) {
110
259
  // Docs are typically in the midnight-docs repo
111
260
  filter.repository = "midnightntwrk/midnight-docs";
112
261
  }
113
- const results = await vectorStore.search(input.query, input.limit, filter);
114
- return {
262
+ const results = await vectorStore.search(sanitizedQuery, limit, filter);
263
+ const response = {
115
264
  results: results.map((r) => ({
116
265
  content: r.content,
117
266
  relevanceScore: r.score,
@@ -122,9 +271,14 @@ export async function searchDocs(input) {
122
271
  },
123
272
  })),
124
273
  totalResults: results.length,
125
- query: input.query,
274
+ query: sanitizedQuery,
126
275
  category: input.category,
276
+ ...(queryValidation.warnings.length > 0 && {
277
+ warnings: queryValidation.warnings,
278
+ }),
127
279
  };
280
+ searchCache.set(cacheKey, response);
281
+ return response;
128
282
  }
129
283
  // Tool definitions for MCP
130
284
  export const searchTools = [
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Generic caching utilities for MCP server
3
+ * Provides TTL-based caching with memory management
4
+ */
5
+ export interface CacheOptions {
6
+ ttl: number;
7
+ maxSize?: number;
8
+ name?: string;
9
+ }
10
+ export interface CacheEntry<T> {
11
+ value: T;
12
+ expiresAt: number;
13
+ createdAt: number;
14
+ }
15
+ export interface CacheStats {
16
+ hits: number;
17
+ misses: number;
18
+ size: number;
19
+ hitRate: number;
20
+ }
21
+ /**
22
+ * Generic cache implementation with TTL and size limits
23
+ */
24
+ export declare class Cache<T> {
25
+ private cache;
26
+ private options;
27
+ private stats;
28
+ constructor(options: CacheOptions);
29
+ /**
30
+ * Get a value from the cache
31
+ */
32
+ get(key: string): T | undefined;
33
+ /**
34
+ * Set a value in the cache
35
+ */
36
+ set(key: string, value: T, ttl?: number): void;
37
+ /**
38
+ * Check if a key exists and is not expired
39
+ */
40
+ has(key: string): boolean;
41
+ /**
42
+ * Delete a key from the cache
43
+ */
44
+ delete(key: string): boolean;
45
+ /**
46
+ * Clear all entries from the cache
47
+ */
48
+ clear(): void;
49
+ /**
50
+ * Remove expired entries
51
+ */
52
+ prune(): number;
53
+ /**
54
+ * Evict the oldest entry to make room
55
+ */
56
+ private evictOldest;
57
+ /**
58
+ * Get cache statistics
59
+ */
60
+ getStats(): CacheStats;
61
+ /**
62
+ * Get or set with a factory function
63
+ */
64
+ getOrSet(key: string, factory: () => Promise<T>, ttl?: number): Promise<T>;
65
+ }
66
+ /**
67
+ * Create a cache key from multiple parts
68
+ */
69
+ export declare function createCacheKey(...parts: (string | number | boolean | undefined)[]): string;
70
+ export declare const searchCache: Cache<unknown>;
71
+ export declare const fileCache: Cache<string>;
72
+ export declare const metadataCache: Cache<unknown>;
73
+ /**
74
+ * Prune all caches periodically
75
+ */
76
+ export declare function pruneAllCaches(): void;
77
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Generic caching utilities for MCP server
3
+ * Provides TTL-based caching with memory management
4
+ */
5
+ import { logger } from "./logger.js";
6
+ /**
7
+ * Generic cache implementation with TTL and size limits
8
+ */
9
+ export class Cache {
10
+ cache = new Map();
11
+ options;
12
+ stats = { hits: 0, misses: 0 };
13
+ constructor(options) {
14
+ this.options = {
15
+ ttl: options.ttl,
16
+ maxSize: options.maxSize || 1000,
17
+ name: options.name || "cache",
18
+ };
19
+ }
20
+ /**
21
+ * Get a value from the cache
22
+ */
23
+ get(key) {
24
+ const entry = this.cache.get(key);
25
+ if (!entry) {
26
+ this.stats.misses++;
27
+ return undefined;
28
+ }
29
+ // Check if expired
30
+ if (Date.now() > entry.expiresAt) {
31
+ this.cache.delete(key);
32
+ this.stats.misses++;
33
+ return undefined;
34
+ }
35
+ this.stats.hits++;
36
+ return entry.value;
37
+ }
38
+ /**
39
+ * Set a value in the cache
40
+ */
41
+ set(key, value, ttl) {
42
+ // Enforce size limit
43
+ if (this.cache.size >= this.options.maxSize) {
44
+ this.evictOldest();
45
+ }
46
+ const now = Date.now();
47
+ this.cache.set(key, {
48
+ value,
49
+ expiresAt: now + (ttl || this.options.ttl),
50
+ createdAt: now,
51
+ });
52
+ }
53
+ /**
54
+ * Check if a key exists and is not expired
55
+ */
56
+ has(key) {
57
+ const entry = this.cache.get(key);
58
+ if (!entry)
59
+ return false;
60
+ if (Date.now() > entry.expiresAt) {
61
+ this.cache.delete(key);
62
+ return false;
63
+ }
64
+ return true;
65
+ }
66
+ /**
67
+ * Delete a key from the cache
68
+ */
69
+ delete(key) {
70
+ return this.cache.delete(key);
71
+ }
72
+ /**
73
+ * Clear all entries from the cache
74
+ */
75
+ clear() {
76
+ this.cache.clear();
77
+ logger.debug(`Cache cleared: ${this.options.name}`);
78
+ }
79
+ /**
80
+ * Remove expired entries
81
+ */
82
+ prune() {
83
+ const now = Date.now();
84
+ let pruned = 0;
85
+ for (const [key, entry] of this.cache.entries()) {
86
+ if (now > entry.expiresAt) {
87
+ this.cache.delete(key);
88
+ pruned++;
89
+ }
90
+ }
91
+ if (pruned > 0) {
92
+ logger.debug(`Cache pruned: ${this.options.name}`, { pruned });
93
+ }
94
+ return pruned;
95
+ }
96
+ /**
97
+ * Evict the oldest entry to make room
98
+ */
99
+ evictOldest() {
100
+ let oldestKey = null;
101
+ let oldestTime = Infinity;
102
+ for (const [key, entry] of this.cache.entries()) {
103
+ if (entry.createdAt < oldestTime) {
104
+ oldestTime = entry.createdAt;
105
+ oldestKey = key;
106
+ }
107
+ }
108
+ if (oldestKey) {
109
+ this.cache.delete(oldestKey);
110
+ }
111
+ }
112
+ /**
113
+ * Get cache statistics
114
+ */
115
+ getStats() {
116
+ const total = this.stats.hits + this.stats.misses;
117
+ return {
118
+ hits: this.stats.hits,
119
+ misses: this.stats.misses,
120
+ size: this.cache.size,
121
+ hitRate: total > 0 ? this.stats.hits / total : 0,
122
+ };
123
+ }
124
+ /**
125
+ * Get or set with a factory function
126
+ */
127
+ async getOrSet(key, factory, ttl) {
128
+ const cached = this.get(key);
129
+ if (cached !== undefined) {
130
+ return cached;
131
+ }
132
+ const value = await factory();
133
+ this.set(key, value, ttl);
134
+ return value;
135
+ }
136
+ }
137
+ /**
138
+ * Create a cache key from multiple parts
139
+ */
140
+ export function createCacheKey(...parts) {
141
+ return parts
142
+ .filter((p) => p !== undefined)
143
+ .map((p) => String(p))
144
+ .join(":");
145
+ }
146
+ // Pre-configured caches for common use cases
147
+ export const searchCache = new Cache({
148
+ ttl: 5 * 60 * 1000, // 5 minutes
149
+ maxSize: 500,
150
+ name: "search",
151
+ });
152
+ export const fileCache = new Cache({
153
+ ttl: 10 * 60 * 1000, // 10 minutes
154
+ maxSize: 200,
155
+ name: "file",
156
+ });
157
+ export const metadataCache = new Cache({
158
+ ttl: 15 * 60 * 1000, // 15 minutes
159
+ maxSize: 100,
160
+ name: "metadata",
161
+ });
162
+ /**
163
+ * Prune all caches periodically
164
+ */
165
+ export function pruneAllCaches() {
166
+ searchCache.prune();
167
+ fileCache.prune();
168
+ metadataCache.prune();
169
+ }
170
+ // Auto-prune every 5 minutes
171
+ setInterval(pruneAllCaches, 5 * 60 * 1000);
172
+ //# sourceMappingURL=cache.js.map