@zokizuan/satori-mcp 4.8.0 → 4.9.0

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
@@ -157,7 +157,7 @@ No parameters.
157
157
  "mcpServers": {
158
158
  "satori": {
159
159
  "command": "npx",
160
- "args": ["-y", "@zokizuan/satori-mcp@4.8.0"],
160
+ "args": ["-y", "@zokizuan/satori-mcp@4.9.0"],
161
161
  "timeout": 180000,
162
162
  "env": {
163
163
  "EMBEDDING_PROVIDER": "VoyageAI",
@@ -178,11 +178,13 @@ No parameters.
178
178
  ```toml
179
179
  [mcp_servers.satori]
180
180
  command = "npx"
181
- args = ["-y", "@zokizuan/satori-mcp@4.8.0"]
181
+ args = ["-y", "@zokizuan/satori-mcp@4.9.0"]
182
182
  startup_timeout_ms = 180000
183
183
  env = { EMBEDDING_PROVIDER = "VoyageAI", EMBEDDING_MODEL = "voyage-4-large", EMBEDDING_OUTPUT_DIMENSION = "1024", VOYAGEAI_API_KEY = "your-api-key", VOYAGEAI_RERANKER_MODEL = "rerank-2.5", MILVUS_ADDRESS = "your-milvus-endpoint", MILVUS_TOKEN = "your-milvus-token" }
184
184
  ```
185
185
 
186
+ `MILVUS_TOKEN` is optional auth for endpoints that require it; local unauthenticated Milvus only needs `MILVUS_ADDRESS`.
187
+
186
188
  ### Local development (when working on this repo)
187
189
 
188
190
  ```json
@@ -228,10 +230,11 @@ Supported installer targets in Phase 1:
228
230
  Examples:
229
231
 
230
232
  ```bash
231
- npx -y @zokizuan/satori-cli@0.2.0 install --client codex
232
- npx -y @zokizuan/satori-cli@0.2.0 install --client claude
233
- npx -y @zokizuan/satori-cli@0.2.0 install --client all --dry-run
234
- npx -y @zokizuan/satori-cli@0.2.0 uninstall --client codex
233
+ npx -y @zokizuan/satori-cli@0.3.0 install --client codex
234
+ npx -y @zokizuan/satori-cli@0.3.0 install --client claude
235
+ npx -y @zokizuan/satori-cli@0.3.0 install --client all --dry-run
236
+ npx -y @zokizuan/satori-cli@0.3.0 uninstall --client codex
237
+ npx -y @zokizuan/satori-cli@0.3.0 doctor
235
238
  ```
236
239
 
237
240
  Install and uninstall run before MCP session startup, only touch Satori-managed config, and copy/remove these packaged skills:
@@ -286,6 +289,10 @@ When spawned by `satori-cli`, server process mode is `SATORI_RUN_MODE=cli`:
286
289
 
287
290
  `SATORI_CLI_STDOUT_GUARD=drop|redirect` controls accidental non-protocol stdout handling (`drop` default).
288
291
 
292
+ ### Startup vs Provider Setup
293
+
294
+ MCP startup does not require provider credentials, network access, or a live Milvus backend. The server should complete `initialize` and expose the six tools with an empty provider environment. Provider-backed calls (`manage_index create|reindex|sync|clear` and `search_codebase`) validate their required environment at call time and return `MISSING_PROVIDER_CONFIG` when setup is incomplete.
295
+
289
296
  ## Development
290
297
 
291
298
  ```bash
@@ -69,7 +69,7 @@ function buildCodexManagedBlock(packageSpecifier) {
69
69
  MANAGED_BLOCK_START,
70
70
  "[mcp_servers.satori]",
71
71
  'command = "npx"',
72
- `args = ["-y", "--package", "${packageSpecifier}", "satori"]`,
72
+ `args = ["-y", "${packageSpecifier}"]`,
73
73
  `startup_timeout_ms = ${MANAGED_TIMEOUT_MS}`,
74
74
  MANAGED_BLOCK_END,
75
75
  "",
@@ -153,10 +153,13 @@ function parseJsonObject(filePath) {
153
153
  function buildClaudeServerConfig(packageSpecifier) {
154
154
  return {
155
155
  command: "npx",
156
- args: ["-y", "--package", packageSpecifier, "satori"],
156
+ args: ["-y", packageSpecifier],
157
157
  timeout: MANAGED_TIMEOUT_MS,
158
158
  };
159
159
  }
160
+ function isManagedPackageSpecifier(value) {
161
+ return typeof value === "string" && /^@zokizuan\/satori-mcp@.+$/.test(value);
162
+ }
160
163
  function isManagedClaudeEntry(value) {
161
164
  if (!value || typeof value !== "object" || Array.isArray(value)) {
162
165
  return false;
@@ -168,14 +171,19 @@ function isManagedClaudeEntry(value) {
168
171
  if (entry.timeout !== MANAGED_TIMEOUT_MS) {
169
172
  return false;
170
173
  }
171
- if (!Array.isArray(entry.args) || entry.args.length !== 4) {
174
+ if (!Array.isArray(entry.args)) {
172
175
  return false;
173
176
  }
174
- return entry.args[0] === "-y"
175
- && entry.args[1] === "--package"
176
- && typeof entry.args[2] === "string"
177
- && /^@zokizuan\/satori-mcp@.+$/.test(entry.args[2])
178
- && entry.args[3] === "satori";
177
+ if (entry.args.length === 2) {
178
+ return entry.args[0] === "-y" && isManagedPackageSpecifier(entry.args[1]);
179
+ }
180
+ if (entry.args.length === 4) {
181
+ return entry.args[0] === "-y"
182
+ && entry.args[1] === "--package"
183
+ && isManagedPackageSpecifier(entry.args[2])
184
+ && entry.args[3] === "satori";
185
+ }
186
+ return false;
179
187
  }
180
188
  function prepareClaudeInstall(filePath, packageSpecifier) {
181
189
  const currentObject = parseJsonObject(filePath);
package/dist/config.js CHANGED
@@ -119,8 +119,8 @@ export function createMcpConfig() {
119
119
  // Ollama configuration
120
120
  ollamaEncoderModel: envManager.get('OLLAMA_MODEL'),
121
121
  ollamaEndpoint: envManager.get('OLLAMA_HOST'),
122
- // Vector database configuration - address can be auto-resolved from token
123
- milvusEndpoint: envManager.get('MILVUS_ADDRESS'), // Optional, can be resolved from token
122
+ // Vector database configuration
123
+ milvusEndpoint: envManager.get('MILVUS_ADDRESS'),
124
124
  milvusApiToken: envManager.get('MILVUS_TOKEN'),
125
125
  // Reranker configuration
126
126
  rankerModel,
@@ -139,7 +139,7 @@ export function logConfigurationSummary(config) {
139
139
  console.log(`[MCP] Server: ${config.name} v${config.version}`);
140
140
  console.log(`[MCP] Embedding Provider: ${config.encoderProvider}`);
141
141
  console.log(`[MCP] Embedding Model: ${config.encoderModel}`);
142
- console.log(`[MCP] Milvus Address: ${config.milvusEndpoint || (config.milvusApiToken ? '[Auto-resolve from token]' : '[Not configured]')}`);
142
+ console.log(`[MCP] Milvus Address: ${config.milvusEndpoint || '[Not configured]'}`);
143
143
  console.log(`[MCP] Proactive Watcher: ${config.watchSyncEnabled ? `enabled (${config.watchDebounceMs || DEFAULT_WATCH_DEBOUNCE_MS}ms debounce)` : 'disabled'}`);
144
144
  // Log provider-specific configuration without exposing sensitive data
145
145
  switch (config.encoderProvider) {
@@ -169,7 +169,7 @@ export function showHelpMessage() {
169
169
  console.log(`
170
170
  Satori MCP Server
171
171
 
172
- Usage: npx -y @zokizuan/satori-mcp@4.8.0 [options]
172
+ Usage: npx -y @zokizuan/satori-mcp@4.9.0 [options]
173
173
 
174
174
  Options:
175
175
  --help, -h Show this help message
@@ -194,8 +194,8 @@ Environment Variables:
194
194
  OLLAMA_MODEL Ollama model name (alternative to EMBEDDING_MODEL for Ollama)
195
195
 
196
196
  Vector Database Configuration:
197
- MILVUS_ADDRESS Milvus address (optional, can be auto-resolved from token)
198
- MILVUS_TOKEN Milvus token (optional, used for authentication and address resolution)
197
+ MILVUS_ADDRESS Milvus address (required for index/search/clear tool calls)
198
+ MILVUS_TOKEN Milvus token (optional, used for authenticated endpoints)
199
199
 
200
200
  Read File Configuration:
201
201
  READ_FILE_MAX_LINES Max lines returned by read_file when no explicit range is provided (default: 1000)
@@ -206,16 +206,16 @@ Environment Variables:
206
206
 
207
207
  Examples:
208
208
  # Start MCP server with OpenAI and explicit Milvus address
209
- OPENAI_API_KEY=sk-xxx MILVUS_ADDRESS=localhost:19530 npx -y @zokizuan/satori-mcp@4.8.0
209
+ OPENAI_API_KEY=sk-xxx MILVUS_ADDRESS=localhost:19530 npx -y @zokizuan/satori-mcp@4.9.0
210
210
 
211
211
  # Start MCP server with VoyageAI and specific model
212
- EMBEDDING_PROVIDER=VoyageAI VOYAGEAI_API_KEY=pa-xxx EMBEDDING_MODEL=voyage-4-large MILVUS_TOKEN=your-token npx -y @zokizuan/satori-mcp@4.8.0
212
+ EMBEDDING_PROVIDER=VoyageAI VOYAGEAI_API_KEY=pa-xxx EMBEDDING_MODEL=voyage-4-large MILVUS_ADDRESS=https://your-zilliz-endpoint MILVUS_TOKEN=your-token npx -y @zokizuan/satori-mcp@4.9.0
213
213
 
214
214
  # Start MCP server with Gemini and specific model
215
- EMBEDDING_PROVIDER=Gemini GEMINI_API_KEY=xxx EMBEDDING_MODEL=gemini-embedding-001 MILVUS_TOKEN=your-token npx -y @zokizuan/satori-mcp@4.8.0
215
+ EMBEDDING_PROVIDER=Gemini GEMINI_API_KEY=xxx EMBEDDING_MODEL=gemini-embedding-001 MILVUS_ADDRESS=https://your-zilliz-endpoint MILVUS_TOKEN=your-token npx -y @zokizuan/satori-mcp@4.9.0
216
216
 
217
217
  # Start MCP server with Ollama and specific model
218
- EMBEDDING_PROVIDER=Ollama EMBEDDING_MODEL=nomic-embed-text MILVUS_TOKEN=your-token npx -y @zokizuan/satori-mcp@4.8.0
218
+ EMBEDDING_PROVIDER=Ollama EMBEDDING_MODEL=nomic-embed-text MILVUS_ADDRESS=localhost:19530 npx -y @zokizuan/satori-mcp@4.9.0
219
219
  `);
220
220
  }
221
221
  //# sourceMappingURL=config.js.map
@@ -15,7 +15,7 @@ export class CapabilityResolver {
15
15
  else {
16
16
  performanceProfile = 'standard';
17
17
  }
18
- const hasVectorStore = Boolean(this.config.milvusEndpoint || this.config.milvusApiToken);
18
+ const hasVectorStore = Boolean(this.config.milvusEndpoint);
19
19
  const hasReranker = Boolean(this.config.voyageKey);
20
20
  const defaultSearchLimit = performanceProfile === 'fast' ? 50 :
21
21
  performanceProfile === 'standard' ? 25 :
@@ -1,7 +1,7 @@
1
1
  import { WarningCode } from "./warnings.js";
2
2
  export type ManageIndexAction = "create" | "reindex" | "sync" | "status" | "clear";
3
3
  export type ManageIndexStatus = "ok" | "not_ready" | "not_indexed" | "requires_reindex" | "blocked" | "error";
4
- export type ManageIndexReason = "indexing" | "not_indexed" | "requires_reindex" | "unnecessary_reindex_ignore_only" | "preflight_unknown" | "backend_timeout" | "remote_delete_pending";
4
+ export type ManageIndexReason = "indexing" | "not_indexed" | "requires_reindex" | "unnecessary_reindex_ignore_only" | "preflight_unknown" | "backend_timeout" | "remote_delete_pending" | "missing_provider_config";
5
5
  export type ManageReindexPreflightOutcome = "reindex_required" | "reindex_unnecessary_ignore_only" | "unknown" | "probe_failed";
6
6
  export interface ManageIndexToolHint {
7
7
  tool: "manage_index";
@@ -14,6 +14,7 @@ export interface ManageIndexResponseEnvelope {
14
14
  path: string;
15
15
  status: ManageIndexStatus;
16
16
  reason?: ManageIndexReason;
17
+ code?: "MISSING_PROVIDER_CONFIG";
17
18
  message: string;
18
19
  humanText: string;
19
20
  warnings?: WarningCode[];
@@ -198,10 +198,11 @@ export interface SearchResponseHints extends Record<string, unknown> {
198
198
  noiseMitigation?: SearchNoiseMitigationHint;
199
199
  debugSearch?: SearchDebugHint;
200
200
  }
201
- export type NonOkReason = "indexing" | "requires_reindex" | "not_indexed";
201
+ export type NonOkReason = "indexing" | "requires_reindex" | "not_indexed" | "missing_provider_config";
202
202
  interface SearchBaseResponseEnvelope {
203
203
  status: "ok" | "requires_reindex" | "not_indexed" | "not_ready";
204
204
  reason?: NonOkReason;
205
+ code?: "MISSING_PROVIDER_CONFIG";
205
206
  path: string;
206
207
  query: string;
207
208
  scope: SearchScope;
package/dist/index.js CHANGED
@@ -3,6 +3,10 @@ import { Writable } from "node:stream";
3
3
  let activeServer = null;
4
4
  let shuttingDown = false;
5
5
  let guardDisabledWarningEmitted = false;
6
+ const bootstrapKeepAlive = setInterval(() => {
7
+ // Node may otherwise exit before top-level async ESM imports finish and stdio transport connects.
8
+ // The server owns steady-state lifetime after startMcpServerFromEnv resolves.
9
+ }, 60 * 60 * 1000);
6
10
  function resolveRunMode() {
7
11
  return process.env.SATORI_RUN_MODE === "cli" ? "cli" : "mcp";
8
12
  }
@@ -79,7 +83,10 @@ process.on("SIGINT", () => {
79
83
  process.on("SIGTERM", () => {
80
84
  void handleShutdownSignal("SIGTERM");
81
85
  });
82
- main().catch((error) => {
86
+ main().then(() => {
87
+ clearInterval(bootstrapKeepAlive);
88
+ }).catch((error) => {
89
+ clearInterval(bootstrapKeepAlive);
83
90
  const message = error instanceof Error ? error.message : String(error);
84
91
  if (message.includes("E_PROTOCOL_FAILURE")) {
85
92
  console.error(`E_PROTOCOL_FAILURE ${message}`);
@@ -0,0 +1,38 @@
1
+ import { Context } from "@zokizuan/satori-core";
2
+ import { CapabilityResolver } from "../core/capabilities.js";
3
+ import { CallGraphSidecarManager } from "../core/call-graph.js";
4
+ import { SnapshotManager } from "../core/snapshot.js";
5
+ import { ContextMcpConfig, IndexFingerprint } from "../config.js";
6
+ import { MissingProviderConfigIssue, ProviderBackedOperation, ToolContext } from "../tools/types.js";
7
+ export declare function resolveConfiguredEmbeddingDimension(config: ContextMcpConfig): number;
8
+ export declare function createLocalOnlyContext(config: ContextMcpConfig): Context;
9
+ export declare class ProviderRuntime {
10
+ private readonly config;
11
+ private readonly snapshotManager;
12
+ private readonly runtimeFingerprint;
13
+ private readonly capabilities;
14
+ private readonly readFileMaxLines;
15
+ private readonly watchSyncEnabled;
16
+ private readonly watchDebounceMs;
17
+ private readonly callGraphManager;
18
+ private readonly now;
19
+ private embeddingRuntimePromise;
20
+ private vectorRuntimePromise;
21
+ private activeContexts;
22
+ constructor(args: {
23
+ config: ContextMcpConfig;
24
+ snapshotManager: SnapshotManager;
25
+ runtimeFingerprint: IndexFingerprint;
26
+ capabilities: CapabilityResolver;
27
+ readFileMaxLines: number;
28
+ watchSyncEnabled: boolean;
29
+ watchDebounceMs: number;
30
+ callGraphManager: CallGraphSidecarManager;
31
+ now?: () => number;
32
+ });
33
+ validate(operation: ProviderBackedOperation): MissingProviderConfigIssue | null;
34
+ requireToolContext(operation: ProviderBackedOperation): Promise<ToolContext | MissingProviderConfigIssue>;
35
+ private createRuntime;
36
+ shutdown(): Promise<void>;
37
+ }
38
+ //# sourceMappingURL=provider-runtime.d.ts.map
@@ -0,0 +1,211 @@
1
+ import { Context, Embedding, MilvusVectorDatabase, VoyageAIReranker, } from "@zokizuan/satori-core";
2
+ import { ToolHandlers } from "../core/handlers.js";
3
+ import { SyncManager } from "../core/sync.js";
4
+ import { createEmbeddingInstance, logEmbeddingProviderInfo } from "../embedding.js";
5
+ class MetadataOnlyEmbedding extends Embedding {
6
+ constructor(provider, dimension) {
7
+ super();
8
+ this.maxTokens = 1;
9
+ this.provider = provider;
10
+ this.dimension = dimension;
11
+ }
12
+ async detectDimension() {
13
+ return this.dimension;
14
+ }
15
+ async embed(_text) {
16
+ throw new Error("MISSING_PROVIDER_CONFIG embedding provider is not configured");
17
+ }
18
+ async embedBatch(_texts) {
19
+ throw new Error("MISSING_PROVIDER_CONFIG embedding provider is not configured");
20
+ }
21
+ getDimension() {
22
+ return this.dimension;
23
+ }
24
+ getProvider() {
25
+ return this.provider;
26
+ }
27
+ }
28
+ class UnconfiguredVectorDatabase {
29
+ throwMissing() {
30
+ throw new Error("MISSING_PROVIDER_CONFIG MILVUS_ADDRESS is not configured");
31
+ }
32
+ async createCollection() { this.throwMissing(); }
33
+ async createHybridCollection() { this.throwMissing(); }
34
+ async dropCollection() { this.throwMissing(); }
35
+ async hasCollection() { this.throwMissing(); }
36
+ async listCollections() { this.throwMissing(); }
37
+ async insert() { this.throwMissing(); }
38
+ async insertHybrid() { this.throwMissing(); }
39
+ async search() { this.throwMissing(); }
40
+ async hybridSearch() { this.throwMissing(); }
41
+ async delete() { this.throwMissing(); }
42
+ async query() { this.throwMissing(); }
43
+ async checkCollectionLimit() { this.throwMissing(); }
44
+ }
45
+ // Local-only startup scaffolding: these satisfy Context/ToolHandlers constructor
46
+ // contracts for provider-free tools. They must not perform provider I/O.
47
+ // Provider-backed tools must use ProviderRuntime.requireToolContext instead.
48
+ export function resolveConfiguredEmbeddingDimension(config) {
49
+ switch (config.encoderProvider) {
50
+ case "OpenAI":
51
+ if (config.encoderModel === "text-embedding-3-large")
52
+ return 3072;
53
+ return 1536;
54
+ case "Gemini":
55
+ return 3072;
56
+ case "Ollama":
57
+ return 768;
58
+ case "VoyageAI":
59
+ default:
60
+ return config.encoderOutputDimension || 1024;
61
+ }
62
+ }
63
+ export function createLocalOnlyContext(config) {
64
+ return new Context({
65
+ embedding: new MetadataOnlyEmbedding(config.encoderProvider, resolveConfiguredEmbeddingDimension(config)),
66
+ vectorDatabase: new UnconfiguredVectorDatabase(),
67
+ });
68
+ }
69
+ function createMissingConfigIssue(missingEnv) {
70
+ const uniqueMissing = [...new Set(missingEnv)];
71
+ const message = `Satori provider setup is incomplete. Missing required environment variable(s): ${uniqueMissing.join(", ")}. MCP startup does not require provider credentials, but this tool call does.`;
72
+ return {
73
+ ok: false,
74
+ code: "MISSING_PROVIDER_CONFIG",
75
+ missingEnv: uniqueMissing,
76
+ message,
77
+ hints: {
78
+ setup: {
79
+ code: "MISSING_PROVIDER_CONFIG",
80
+ missingEnv: uniqueMissing,
81
+ nextSteps: uniqueMissing.map((name) => `Set ${name}, restart the MCP server, then retry the tool call.`),
82
+ }
83
+ }
84
+ };
85
+ }
86
+ export class ProviderRuntime {
87
+ constructor(args) {
88
+ this.embeddingRuntimePromise = null;
89
+ this.vectorRuntimePromise = null;
90
+ this.activeContexts = [];
91
+ this.config = args.config;
92
+ this.snapshotManager = args.snapshotManager;
93
+ this.runtimeFingerprint = args.runtimeFingerprint;
94
+ this.capabilities = args.capabilities;
95
+ this.readFileMaxLines = args.readFileMaxLines;
96
+ this.watchSyncEnabled = args.watchSyncEnabled;
97
+ this.watchDebounceMs = args.watchDebounceMs;
98
+ this.callGraphManager = args.callGraphManager;
99
+ this.now = args.now || (() => Date.now());
100
+ }
101
+ validate(operation) {
102
+ const missing = [];
103
+ if (operation === "embedding_vector") {
104
+ switch (this.config.encoderProvider) {
105
+ case "OpenAI":
106
+ if (!this.config.openaiKey)
107
+ missing.push("OPENAI_API_KEY");
108
+ break;
109
+ case "VoyageAI":
110
+ if (!this.config.voyageKey)
111
+ missing.push("VOYAGEAI_API_KEY");
112
+ break;
113
+ case "Gemini":
114
+ if (!this.config.geminiKey)
115
+ missing.push("GEMINI_API_KEY");
116
+ break;
117
+ case "Ollama":
118
+ break;
119
+ }
120
+ }
121
+ if (!this.config.milvusEndpoint) {
122
+ missing.push("MILVUS_ADDRESS");
123
+ }
124
+ return missing.length > 0 ? createMissingConfigIssue(missing) : null;
125
+ }
126
+ async requireToolContext(operation) {
127
+ const validation = this.validate(operation);
128
+ if (validation) {
129
+ return validation;
130
+ }
131
+ if (operation === "vector_only") {
132
+ if (!this.vectorRuntimePromise) {
133
+ this.vectorRuntimePromise = this.createRuntime(false).catch((error) => {
134
+ this.vectorRuntimePromise = null;
135
+ throw error;
136
+ });
137
+ }
138
+ return this.vectorRuntimePromise;
139
+ }
140
+ if (!this.embeddingRuntimePromise) {
141
+ this.embeddingRuntimePromise = this.createRuntime(true).catch((error) => {
142
+ this.embeddingRuntimePromise = null;
143
+ throw error;
144
+ });
145
+ }
146
+ return this.embeddingRuntimePromise;
147
+ }
148
+ async createRuntime(requireEmbedding) {
149
+ const embedding = requireEmbedding
150
+ ? createEmbeddingInstance(this.config)
151
+ : new MetadataOnlyEmbedding(this.config.encoderProvider, resolveConfiguredEmbeddingDimension(this.config));
152
+ if (requireEmbedding) {
153
+ logEmbeddingProviderInfo(this.config, embedding);
154
+ }
155
+ const vectorDatabase = new MilvusVectorDatabase({
156
+ address: this.config.milvusEndpoint,
157
+ ...(this.config.milvusApiToken && { token: this.config.milvusApiToken }),
158
+ });
159
+ const context = new Context({
160
+ embedding,
161
+ vectorDatabase,
162
+ });
163
+ const syncManager = new SyncManager(context, this.snapshotManager, {
164
+ watchEnabled: this.watchSyncEnabled,
165
+ watchDebounceMs: this.watchDebounceMs,
166
+ onSyncCompleted: async (codebasePath, stats) => {
167
+ try {
168
+ const sidecar = await this.callGraphManager.rebuildIfSupportedDelta(codebasePath, stats.changedFiles, context.getActiveIgnorePatterns(codebasePath));
169
+ if (sidecar) {
170
+ this.snapshotManager.setCodebaseCallGraphSidecar(codebasePath, sidecar);
171
+ this.snapshotManager.saveCodebaseSnapshot();
172
+ console.log(`[CALL-GRAPH] Rebuilt sidecar for '${codebasePath}' from sync lifecycle callback.`);
173
+ }
174
+ }
175
+ catch (error) {
176
+ console.warn(`[CALL-GRAPH] Sync lifecycle rebuild failed for '${codebasePath}': ${error?.message || error}`);
177
+ }
178
+ }
179
+ });
180
+ const reranker = requireEmbedding && this.capabilities.hasReranker()
181
+ ? new VoyageAIReranker({
182
+ apiKey: this.config.voyageKey,
183
+ model: this.config.rankerModel || "rerank-2.5",
184
+ })
185
+ : null;
186
+ if (reranker) {
187
+ console.log(`[RERANKER] VoyageAI Reranker initialized with model: ${this.config.rankerModel || "rerank-2.5"}`);
188
+ }
189
+ const toolHandlers = new ToolHandlers(context, this.snapshotManager, syncManager, this.runtimeFingerprint, this.capabilities, this.now, this.callGraphManager, reranker);
190
+ const toolContext = {
191
+ context,
192
+ snapshotManager: this.snapshotManager,
193
+ syncManager,
194
+ capabilities: this.capabilities,
195
+ reranker,
196
+ runtimeFingerprint: this.runtimeFingerprint,
197
+ toolHandlers,
198
+ readFileMaxLines: this.readFileMaxLines,
199
+ providerRuntime: this,
200
+ };
201
+ this.activeContexts.push(toolContext);
202
+ return toolContext;
203
+ }
204
+ async shutdown() {
205
+ await Promise.all(this.activeContexts.map(async (toolContext) => {
206
+ toolContext.syncManager.stopBackgroundSync();
207
+ await toolContext.syncManager.stopWatcherMode();
208
+ }));
209
+ }
210
+ }
211
+ //# sourceMappingURL=provider-runtime.js.map
@@ -1,8 +1,9 @@
1
- import { Writable } from "node:stream";
1
+ import { Readable, Writable } from "node:stream";
2
2
  import { ContextMcpConfig } from "../config.js";
3
3
  export type ServerRunMode = "mcp" | "cli";
4
4
  export interface StartMcpServerOptions {
5
5
  runMode?: ServerRunMode;
6
+ protocolStdin?: Readable;
6
7
  protocolStdout?: Writable;
7
8
  args?: string[];
8
9
  }
@@ -19,20 +20,22 @@ interface StartupLifecycleDependencies {
19
20
  export declare function runPostConnectStartupLifecycle(runMode: ServerRunMode, dependencies: StartupLifecycleDependencies): Promise<void>;
20
21
  declare class ContextMcpServer {
21
22
  private server;
22
- private context;
23
+ private toolContext;
23
24
  private snapshotManager;
24
25
  private syncManager;
25
26
  private toolHandlers;
26
- private reranker;
27
27
  private capabilities;
28
28
  private runtimeFingerprint;
29
29
  private readFileMaxLines;
30
30
  private watchSyncEnabled;
31
31
  private watchDebounceMs;
32
32
  private callGraphManager;
33
+ private providerRuntime;
33
34
  private runMode;
35
+ private protocolStdin?;
34
36
  private protocolStdout?;
35
- constructor(config: ContextMcpConfig, runMode: ServerRunMode, protocolStdout?: Writable);
37
+ private keepAliveTimer;
38
+ constructor(config: ContextMcpConfig, runMode: ServerRunMode, protocolStdout?: Writable, protocolStdin?: Readable);
36
39
  private getCliTransportStdout;
37
40
  private getToolContext;
38
41
  /**
@@ -4,9 +4,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
4
4
  import fs from "node:fs";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
- import { Context, MilvusVectorDatabase, VoyageAIReranker } from "@zokizuan/satori-core";
8
7
  import { buildRuntimeIndexFingerprint, createMcpConfig, logConfigurationSummary, showHelpMessage, } from "../config.js";
9
- import { createEmbeddingInstance, logEmbeddingProviderInfo } from "../embedding.js";
10
8
  import { CapabilityResolver } from "../core/capabilities.js";
11
9
  import { SnapshotManager } from "../core/snapshot.js";
12
10
  import { SyncManager } from "../core/sync.js";
@@ -14,6 +12,7 @@ import { ToolHandlers } from "../core/handlers.js";
14
12
  import { CallGraphSidecarManager } from "../core/call-graph.js";
15
13
  import { decideInterruptedIndexingRecovery } from "../core/indexing-recovery.js";
16
14
  import { getMcpToolList, toolRegistry } from "../tools/registry.js";
15
+ import { createLocalOnlyContext, ProviderRuntime, resolveConfiguredEmbeddingDimension } from "./provider-runtime.js";
17
16
  export async function runPostConnectStartupLifecycle(runMode, dependencies) {
18
17
  if (runMode === "cli") {
19
18
  try {
@@ -57,9 +56,10 @@ function migrateLegacyStateDir() {
57
56
  }
58
57
  }
59
58
  class ContextMcpServer {
60
- constructor(config, runMode, protocolStdout) {
61
- this.reranker = null;
59
+ constructor(config, runMode, protocolStdout, protocolStdin) {
60
+ this.keepAliveTimer = null;
62
61
  this.runMode = runMode;
62
+ this.protocolStdin = protocolStdin;
63
63
  this.protocolStdout = protocolStdout;
64
64
  this.server = new Server({
65
65
  name: config.name,
@@ -69,100 +69,81 @@ class ContextMcpServer {
69
69
  tools: {},
70
70
  },
71
71
  });
72
- console.log(`[EMBEDDING] Initializing embedding provider: ${config.encoderProvider}`);
73
- console.log(`[EMBEDDING] Using model: ${config.encoderModel}`);
74
- const embedding = createEmbeddingInstance(config);
75
- logEmbeddingProviderInfo(config, embedding);
76
72
  this.capabilities = new CapabilityResolver(config);
77
- this.runtimeFingerprint = buildRuntimeIndexFingerprint(config, embedding.getDimension());
73
+ this.runtimeFingerprint = buildRuntimeIndexFingerprint(config, resolveConfiguredEmbeddingDimension(config));
78
74
  this.readFileMaxLines = Math.max(1, config.readFileMaxLines ?? 1000);
79
75
  this.watchSyncEnabled = config.watchSyncEnabled === true;
80
76
  this.watchDebounceMs = Math.max(1, config.watchDebounceMs ?? 5000);
81
77
  console.log(`[FINGERPRINT] Runtime index fingerprint: ${JSON.stringify(this.runtimeFingerprint)}`);
82
- const vectorDatabase = new MilvusVectorDatabase({
83
- address: config.milvusEndpoint,
84
- ...(config.milvusApiToken && { token: config.milvusApiToken }),
85
- });
86
- this.context = new Context({
87
- embedding,
88
- vectorDatabase,
89
- });
90
78
  this.snapshotManager = new SnapshotManager(this.runtimeFingerprint);
91
79
  this.callGraphManager = new CallGraphSidecarManager(this.runtimeFingerprint);
92
- this.syncManager = new SyncManager(this.context, this.snapshotManager, {
80
+ const localContext = createLocalOnlyContext(config);
81
+ this.syncManager = new SyncManager(localContext, this.snapshotManager, {
93
82
  watchEnabled: this.watchSyncEnabled,
94
83
  watchDebounceMs: this.watchDebounceMs,
95
- onSyncCompleted: async (codebasePath, stats) => {
96
- try {
97
- const sidecar = await this.callGraphManager.rebuildIfSupportedDelta(codebasePath, stats.changedFiles, this.context.getActiveIgnorePatterns(codebasePath));
98
- if (sidecar) {
99
- this.snapshotManager.setCodebaseCallGraphSidecar(codebasePath, sidecar);
100
- this.snapshotManager.saveCodebaseSnapshot();
101
- console.log(`[CALL-GRAPH] Rebuilt sidecar for '${codebasePath}' from sync lifecycle callback.`);
102
- }
103
- }
104
- catch (error) {
105
- console.warn(`[CALL-GRAPH] Sync lifecycle rebuild failed for '${codebasePath}': ${error?.message || error}`);
106
- }
107
- }
108
84
  });
109
- if (this.capabilities.hasReranker()) {
110
- this.reranker = new VoyageAIReranker({
111
- apiKey: config.voyageKey,
112
- model: config.rankerModel || "rerank-2.5",
113
- });
114
- console.log(`[RERANKER] VoyageAI Reranker initialized with model: ${config.rankerModel || "rerank-2.5"}`);
115
- }
116
- this.toolHandlers = new ToolHandlers(this.context, this.snapshotManager, this.syncManager, this.runtimeFingerprint, this.capabilities, () => Date.now(), this.callGraphManager, this.reranker);
85
+ this.toolHandlers = new ToolHandlers(localContext, this.snapshotManager, this.syncManager, this.runtimeFingerprint, this.capabilities, () => Date.now(), this.callGraphManager, null);
86
+ this.providerRuntime = new ProviderRuntime({
87
+ config,
88
+ snapshotManager: this.snapshotManager,
89
+ runtimeFingerprint: this.runtimeFingerprint,
90
+ capabilities: this.capabilities,
91
+ readFileMaxLines: this.readFileMaxLines,
92
+ watchSyncEnabled: this.watchSyncEnabled,
93
+ watchDebounceMs: this.watchDebounceMs,
94
+ callGraphManager: this.callGraphManager,
95
+ });
96
+ this.toolContext = {
97
+ context: localContext,
98
+ snapshotManager: this.snapshotManager,
99
+ syncManager: this.syncManager,
100
+ capabilities: this.capabilities,
101
+ reranker: null,
102
+ runtimeFingerprint: this.runtimeFingerprint,
103
+ toolHandlers: this.toolHandlers,
104
+ readFileMaxLines: this.readFileMaxLines,
105
+ providerRuntime: this.providerRuntime,
106
+ };
117
107
  this.snapshotManager.loadCodebaseSnapshot();
118
108
  this.setupTools();
119
109
  }
120
110
  getCliTransportStdout() {
111
+ if (this.protocolStdout) {
112
+ return this.protocolStdout;
113
+ }
121
114
  if (this.runMode !== "cli") {
122
115
  return process.stdout;
123
116
  }
124
- if (!this.protocolStdout) {
125
- throw new Error("E_PROTOCOL_FAILURE Missing protocolStdout for cli mode");
126
- }
127
- return this.protocolStdout;
117
+ throw new Error("E_PROTOCOL_FAILURE Missing protocolStdout for cli mode");
128
118
  }
129
119
  getToolContext() {
130
- return {
131
- context: this.context,
132
- snapshotManager: this.snapshotManager,
133
- syncManager: this.syncManager,
134
- capabilities: this.capabilities,
135
- reranker: this.reranker,
136
- runtimeFingerprint: this.runtimeFingerprint,
137
- toolHandlers: this.toolHandlers,
138
- readFileMaxLines: this.readFileMaxLines,
139
- };
120
+ return this.toolContext;
140
121
  }
141
122
  /**
142
123
  * Verify cloud state and fix interrupted indexing snapshots.
143
124
  */
144
- async verifyCloudState() {
125
+ async verifyCloudState(toolContext) {
145
126
  console.log("[STARTUP] Verifying interrupted indexing state against completion markers...");
146
- const indexingCodebases = this.snapshotManager.getIndexingCodebases();
127
+ const indexingCodebases = toolContext.snapshotManager.getIndexingCodebases();
147
128
  let promotedCount = 0;
148
129
  let failedCount = 0;
149
130
  for (const codebasePath of indexingCodebases) {
150
- const marker = typeof this.context.getIndexCompletionMarker === "function"
151
- ? await this.context.getIndexCompletionMarker(codebasePath)
131
+ const marker = typeof toolContext.context.getIndexCompletionMarker === "function"
132
+ ? await toolContext.context.getIndexCompletionMarker(codebasePath)
152
133
  : null;
153
134
  const decision = decideInterruptedIndexingRecovery(marker, this.runtimeFingerprint);
154
135
  if (decision.action === "promote_indexed") {
155
- this.snapshotManager.setCodebaseIndexed(codebasePath, decision.stats, this.runtimeFingerprint, "verified");
136
+ toolContext.snapshotManager.setCodebaseIndexed(codebasePath, decision.stats, this.runtimeFingerprint, "verified");
156
137
  promotedCount++;
157
138
  console.log(`[STARTUP] Recovered interrupted indexing from marker: ${codebasePath} -> indexed`);
158
139
  continue;
159
140
  }
160
- this.snapshotManager.setCodebaseIndexFailed(codebasePath, decision.message);
141
+ toolContext.snapshotManager.setCodebaseIndexFailed(codebasePath, decision.message);
161
142
  failedCount++;
162
143
  console.log(`[STARTUP] Marked interrupted indexing as failed: ${codebasePath} (${decision.reason})`);
163
144
  }
164
145
  if (promotedCount > 0 || failedCount > 0) {
165
- this.snapshotManager.saveCodebaseSnapshot();
146
+ toolContext.snapshotManager.saveCodebaseSnapshot();
166
147
  console.log(`[STARTUP] Recovery summary: promoted=${promotedCount}, failed=${failedCount}`);
167
148
  }
168
149
  else {
@@ -192,25 +173,26 @@ class ContextMcpServer {
192
173
  }
193
174
  async start() {
194
175
  console.log("Starting Satori MCP server...");
195
- const transport = this.runMode === "cli"
196
- ? new StdioServerTransport(process.stdin, this.getCliTransportStdout())
176
+ const transportStdin = this.protocolStdin ?? process.stdin;
177
+ const transport = this.runMode === "cli" || this.protocolStdin || this.protocolStdout
178
+ ? new StdioServerTransport(transportStdin, this.getCliTransportStdout())
197
179
  : new StdioServerTransport();
198
180
  await this.server.connect(transport);
181
+ transportStdin.resume();
182
+ this.keepAliveTimer = setInterval(() => {
183
+ // Keep stdio MCP process alive when startup has no background provider lifecycle.
184
+ }, 60 * 60 * 1000);
199
185
  console.log("MCP server started and listening on stdio.");
200
- await runPostConnectStartupLifecycle(this.runMode, {
201
- watchSyncEnabled: this.watchSyncEnabled,
202
- verifyCloudState: () => this.verifyCloudState(),
203
- onVerifyCloudStateError: (error) => {
204
- const message = error instanceof Error ? error.message : String(error);
205
- console.error("[STARTUP] Error verifying cloud state:", message);
206
- },
207
- syncManager: this.syncManager,
208
- });
209
186
  }
210
187
  async shutdown() {
211
188
  console.log("Shutting down Satori MCP server...");
189
+ if (this.keepAliveTimer) {
190
+ clearInterval(this.keepAliveTimer);
191
+ this.keepAliveTimer = null;
192
+ }
212
193
  this.syncManager.stopBackgroundSync();
213
194
  await this.syncManager.stopWatcherMode();
195
+ await this.providerRuntime.shutdown();
214
196
  }
215
197
  }
216
198
  function isHelpRequested(args) {
@@ -226,7 +208,7 @@ export async function startMcpServerFromEnv(options = {}) {
226
208
  migrateLegacyStateDir();
227
209
  const config = createMcpConfig();
228
210
  logConfigurationSummary(config);
229
- const server = new ContextMcpServer(config, runMode, options.protocolStdout);
211
+ const server = new ContextMcpServer(config, runMode, options.protocolStdout, options.protocolStdin);
230
212
  await server.start();
231
213
  return server;
232
214
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { formatZodError } from "./types.js";
3
+ import { formatManageProviderConfigError, isMissingProviderConfigIssue } from "./setup-errors.js";
3
4
  const actionEnum = z.enum(["create", "reindex", "sync", "status", "clear"]);
4
5
  const manageIndexInputSchema = z.object({
5
6
  action: actionEnum.describe("Required operation to run."),
@@ -26,17 +27,28 @@ export const manageIndexTool = {
26
27
  };
27
28
  }
28
29
  const input = parsed.data;
30
+ const providerOperation = input.action === "clear"
31
+ ? "vector_only"
32
+ : (input.action === "create" || input.action === "reindex" || input.action === "sync")
33
+ ? "embedding_vector"
34
+ : null;
35
+ const executionContext = providerOperation && ctx.providerRuntime
36
+ ? await ctx.providerRuntime.requireToolContext(providerOperation)
37
+ : ctx;
38
+ if (isMissingProviderConfigIssue(executionContext)) {
39
+ return formatManageProviderConfigError(input.action, input.path, executionContext);
40
+ }
29
41
  switch (input.action) {
30
42
  case 'create':
31
- return ctx.toolHandlers.handleIndexCodebase(input);
43
+ return executionContext.toolHandlers.handleIndexCodebase(input);
32
44
  case 'reindex':
33
- return ctx.toolHandlers.handleReindexCodebase(input);
45
+ return executionContext.toolHandlers.handleReindexCodebase(input);
34
46
  case 'sync':
35
- return ctx.toolHandlers.handleSyncCodebase(input);
47
+ return executionContext.toolHandlers.handleSyncCodebase(input);
36
48
  case 'status':
37
- return ctx.toolHandlers.handleGetIndexingStatus(input);
49
+ return executionContext.toolHandlers.handleGetIndexingStatus(input);
38
50
  case 'clear':
39
- return ctx.toolHandlers.handleClearIndex(input);
51
+ return executionContext.toolHandlers.handleClearIndex(input);
40
52
  default:
41
53
  return {
42
54
  content: [{
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { formatZodError } from "./types.js";
3
3
  import { emitSearchTelemetry } from "../telemetry/search.js";
4
+ import { formatSearchProviderConfigError, isMissingProviderConfigIssue } from "./setup-errors.js";
4
5
  function getProfile(ctx) {
5
6
  const locality = ctx.capabilities.getEmbeddingLocality();
6
7
  const profile = ctx.capabilities.getPerformanceProfile();
@@ -92,11 +93,43 @@ export const searchCodebaseTool = {
92
93
  isError: true
93
94
  };
94
95
  }
95
- const input = parsed.data;
96
+ const input = {
97
+ ...parsed.data,
98
+ scope: parsed.data.scope ?? "runtime",
99
+ resultMode: parsed.data.resultMode ?? "grouped",
100
+ groupBy: parsed.data.groupBy ?? "symbol",
101
+ rankingMode: parsed.data.rankingMode ?? "auto_changed_first",
102
+ };
96
103
  const startedAt = Date.now();
97
104
  const limit = Math.max(1, Math.min(ctx.capabilities.getMaxSearchLimit(), input.limit ?? ctx.capabilities.getDefaultSearchLimit()));
98
105
  const profile = getProfile(ctx);
99
- const response = await ctx.toolHandlers.handleSearchCode({
106
+ const executionContext = ctx.providerRuntime
107
+ ? await ctx.providerRuntime.requireToolContext("embedding_vector")
108
+ : ctx;
109
+ if (isMissingProviderConfigIssue(executionContext)) {
110
+ const response = formatSearchProviderConfigError({
111
+ ...input,
112
+ limit,
113
+ }, executionContext);
114
+ emitSearchTelemetry({
115
+ event: "search_executed",
116
+ tool_name: "search_codebase",
117
+ profile,
118
+ query_length: input.query.length,
119
+ limit_requested: limit,
120
+ results_before_filter: 0,
121
+ results_after_filter: 0,
122
+ results_returned: 0,
123
+ excluded_by_ignore: 0,
124
+ reranker_used: false,
125
+ reranker_attempted: false,
126
+ latency_ms: Date.now() - startedAt,
127
+ parallel_fanout: true,
128
+ error: executionContext.code,
129
+ });
130
+ return response;
131
+ }
132
+ const response = await executionContext.toolHandlers.handleSearchCode({
100
133
  ...input,
101
134
  limit
102
135
  });
@@ -0,0 +1,14 @@
1
+ import { ManageIndexAction } from "../core/manage-types.js";
2
+ import { SearchGroupBy, SearchResultMode, SearchScope } from "../core/search-constants.js";
3
+ import { MissingProviderConfigIssue, ToolResponse } from "./types.js";
4
+ export declare function isMissingProviderConfigIssue(value: unknown): value is MissingProviderConfigIssue;
5
+ export declare function formatManageProviderConfigError(action: ManageIndexAction, path: string, issue: MissingProviderConfigIssue): ToolResponse;
6
+ export declare function formatSearchProviderConfigError(input: {
7
+ path: string;
8
+ query: string;
9
+ scope: SearchScope;
10
+ groupBy: SearchGroupBy;
11
+ resultMode: SearchResultMode;
12
+ limit: number;
13
+ }, issue: MissingProviderConfigIssue): ToolResponse;
14
+ //# sourceMappingURL=setup-errors.d.ts.map
@@ -0,0 +1,48 @@
1
+ export function isMissingProviderConfigIssue(value) {
2
+ return Boolean(value)
3
+ && typeof value === "object"
4
+ && value.ok === false
5
+ && value.code === "MISSING_PROVIDER_CONFIG";
6
+ }
7
+ export function formatManageProviderConfigError(action, path, issue) {
8
+ return {
9
+ content: [{
10
+ type: "text",
11
+ text: JSON.stringify({
12
+ tool: "manage_index",
13
+ version: 1,
14
+ action,
15
+ path,
16
+ status: "error",
17
+ reason: "missing_provider_config",
18
+ code: issue.code,
19
+ message: issue.message,
20
+ humanText: issue.message,
21
+ hints: issue.hints,
22
+ }, null, 2)
23
+ }]
24
+ };
25
+ }
26
+ export function formatSearchProviderConfigError(input, issue) {
27
+ return {
28
+ content: [{
29
+ type: "text",
30
+ text: JSON.stringify({
31
+ status: "not_ready",
32
+ reason: "missing_provider_config",
33
+ code: issue.code,
34
+ path: input.path,
35
+ query: input.query,
36
+ scope: input.scope,
37
+ groupBy: input.groupBy,
38
+ resultMode: input.resultMode,
39
+ limit: input.limit,
40
+ freshnessDecision: null,
41
+ message: issue.message,
42
+ hints: issue.hints,
43
+ results: [],
44
+ }, null, 2)
45
+ }]
46
+ };
47
+ }
48
+ //# sourceMappingURL=setup-errors.js.map
@@ -5,6 +5,20 @@ import { SnapshotManager } from "../core/snapshot.js";
5
5
  import { SyncManager } from "../core/sync.js";
6
6
  import { IndexFingerprint } from "../config.js";
7
7
  import { ToolHandlers } from "../core/handlers.js";
8
+ export type ProviderBackedOperation = "embedding_vector" | "vector_only";
9
+ export interface MissingProviderConfigIssue {
10
+ ok: false;
11
+ code: "MISSING_PROVIDER_CONFIG";
12
+ missingEnv: string[];
13
+ message: string;
14
+ hints: {
15
+ setup: {
16
+ code: "MISSING_PROVIDER_CONFIG";
17
+ missingEnv: string[];
18
+ nextSteps: string[];
19
+ };
20
+ };
21
+ }
8
22
  export interface ToolResponse {
9
23
  content: Array<{
10
24
  type: string;
@@ -22,6 +36,9 @@ export interface ToolContext {
22
36
  runtimeFingerprint: IndexFingerprint;
23
37
  toolHandlers: ToolHandlers;
24
38
  readFileMaxLines: number;
39
+ providerRuntime?: {
40
+ requireToolContext(operation: ProviderBackedOperation): Promise<ToolContext | MissingProviderConfigIssue>;
41
+ };
25
42
  }
26
43
  export interface McpTool<TSchema extends z.ZodTypeAny = z.ZodTypeAny> {
27
44
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zokizuan/satori-mcp",
3
- "version": "4.8.0",
3
+ "version": "4.9.0",
4
4
  "description": "MCP server for Satori with agent-safe semantic search and indexing",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",