veryfront 0.1.159 → 0.1.162

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.
Files changed (51) hide show
  1. package/esm/deno.js +1 -1
  2. package/esm/src/agent/index.d.ts +1 -1
  3. package/esm/src/agent/index.d.ts.map +1 -1
  4. package/esm/src/agent/runtime/index.d.ts +2 -12
  5. package/esm/src/agent/runtime/index.d.ts.map +1 -1
  6. package/esm/src/agent/runtime/index.js +62 -25
  7. package/esm/src/agent/types.d.ts +37 -0
  8. package/esm/src/agent/types.d.ts.map +1 -1
  9. package/esm/src/mcp/http-transport.d.ts +33 -0
  10. package/esm/src/mcp/http-transport.d.ts.map +1 -0
  11. package/esm/src/mcp/http-transport.js +97 -0
  12. package/esm/src/mcp/server.d.ts.map +1 -1
  13. package/esm/src/mcp/server.js +14 -107
  14. package/esm/src/platform/adapters/fs/veryfront/base-operations.d.ts +1 -1
  15. package/esm/src/platform/adapters/fs/veryfront/base-operations.d.ts.map +1 -1
  16. package/esm/src/platform/adapters/fs/veryfront/directory-operations.d.ts.map +1 -1
  17. package/esm/src/platform/adapters/fs/veryfront/directory-operations.js +9 -52
  18. package/esm/src/platform/adapters/fs/veryfront/file-list-access.d.ts +33 -0
  19. package/esm/src/platform/adapters/fs/veryfront/file-list-access.d.ts.map +1 -0
  20. package/esm/src/platform/adapters/fs/veryfront/file-list-access.js +49 -0
  21. package/esm/src/platform/adapters/fs/veryfront/read-operations.d.ts +1 -20
  22. package/esm/src/platform/adapters/fs/veryfront/read-operations.d.ts.map +1 -1
  23. package/esm/src/platform/adapters/fs/veryfront/stat-operations.d.ts.map +1 -1
  24. package/esm/src/platform/adapters/fs/veryfront/stat-operations.js +21 -94
  25. package/esm/src/tool/factory.d.ts +3 -5
  26. package/esm/src/tool/factory.d.ts.map +1 -1
  27. package/esm/src/tool/factory.js +2 -1
  28. package/esm/src/tool/index.d.ts +2 -0
  29. package/esm/src/tool/index.d.ts.map +1 -1
  30. package/esm/src/tool/index.js +1 -0
  31. package/esm/src/tool/remote-source-tools.d.ts +8 -0
  32. package/esm/src/tool/remote-source-tools.d.ts.map +1 -0
  33. package/esm/src/tool/remote-source-tools.js +33 -0
  34. package/esm/src/utils/version-constant.d.ts +1 -1
  35. package/esm/src/utils/version-constant.js +1 -1
  36. package/package.json +1 -1
  37. package/src/deno.js +1 -1
  38. package/src/src/agent/index.ts +6 -0
  39. package/src/src/agent/runtime/index.ts +118 -11
  40. package/src/src/agent/types.ts +47 -0
  41. package/src/src/mcp/http-transport.ts +163 -0
  42. package/src/src/mcp/server.ts +15 -123
  43. package/src/src/platform/adapters/fs/veryfront/base-operations.ts +1 -1
  44. package/src/src/platform/adapters/fs/veryfront/directory-operations.ts +10 -75
  45. package/src/src/platform/adapters/fs/veryfront/file-list-access.ts +109 -0
  46. package/src/src/platform/adapters/fs/veryfront/read-operations.ts +1 -22
  47. package/src/src/platform/adapters/fs/veryfront/stat-operations.ts +27 -120
  48. package/src/src/tool/factory.ts +4 -6
  49. package/src/src/tool/index.ts +5 -0
  50. package/src/src/tool/remote-source-tools.ts +54 -0
  51. package/src/src/utils/version-constant.ts +1 -1
@@ -9,18 +9,14 @@ import type { MCPServerConfig, ToolListEntry } from "./types.js";
9
9
  import { createError, toError } from "../errors/veryfront-error.js";
10
10
  import { withSpan } from "../observability/tracing/otlp-setup.js";
11
11
  import { VERSION } from "../utils/version.js";
12
- import { validateContentType } from "../security/input-validation/limits.js";
13
- import { VeryfrontError } from "../security/input-validation/errors.js";
14
12
  import type { IntegrationRuntimeConfig } from "../integrations/types.js";
15
13
  import { logger as baseLogger } from "../utils/index.js";
14
+ import { createMCPHTTPHandler } from "./http-transport.js";
16
15
  import { SessionManager } from "./session.js";
17
16
  import { TaskStore } from "./task-store.js";
18
17
 
19
18
  const logger = baseLogger.component("mcp-server");
20
-
21
- const MAX_REQUEST_BODY_SIZE = 1_048_576; // 1 MB
22
19
  const MAX_CONTEXT_HEADER_LENGTH = 255;
23
- const JSON_CONTENT_TYPE = "application/json";
24
20
  const END_USER_ID_PATTERN = /^[a-zA-Z0-9._@-]+$/;
25
21
  const PROJECT_ID_PATTERN = /^[a-zA-Z0-9._-]+$/;
26
22
 
@@ -55,23 +51,6 @@ function toParamsRecord(params: JSONRPCParams | undefined): Record<string, unkno
55
51
  return params;
56
52
  }
57
53
 
58
- function createJSONResponse(body: unknown, init?: dntShim.ResponseInit): dntShim.Response {
59
- const headers = new dntShim.Headers(init?.headers);
60
- headers.set("Content-Type", JSON_CONTENT_TYPE);
61
- return new dntShim.Response(JSON.stringify(body), { ...init, headers });
62
- }
63
-
64
- function createJSONRPCErrorResponse(status: number, code: number, message: string): dntShim.Response {
65
- return createJSONResponse(
66
- {
67
- jsonrpc: "2.0",
68
- id: null,
69
- error: { code, message },
70
- },
71
- { status },
72
- );
73
- }
74
-
75
54
  function readAllowedHeader(
76
55
  request: dntShim.Request,
77
56
  headerName: string,
@@ -628,107 +607,20 @@ export class MCPServer {
628
607
  }
629
608
 
630
609
  createHTTPHandler(): (request: dntShim.Request) => Promise<dntShim.Response> {
631
- return async (request: dntShim.Request) => {
632
- const requestOrigin = request.headers.get("Origin");
633
-
634
- // CORS preflight
635
- if (request.method === "OPTIONS") {
636
- return new dntShim.Response(null, { status: 204, headers: this.getCORSHeaders(requestOrigin) });
637
- }
638
-
639
- // Origin validation (DNS rebinding protection)
640
- if (requestOrigin && this.config.cors?.enabled && this.config.cors.origins?.length) {
641
- if (!this.config.cors.origins.includes(requestOrigin)) {
642
- return createJSONRPCErrorResponse(403, -32600, "Forbidden: Origin not allowed");
643
- }
644
- }
645
-
646
- // Auth check (applies to all methods including DELETE)
647
- if (this.config.auth?.type && this.config.auth.type !== "none") {
648
- const authorized = await this.validateAuth(request);
649
- if (!authorized) return new dntShim.Response("Unauthorized", { status: 401 });
650
- }
651
-
652
- // DELETE = terminate session
653
- if (request.method === "DELETE") {
654
- const sessionId = request.headers.get("MCP-Session-Id");
655
- if (sessionId) {
656
- this.sessionManager.terminate(sessionId);
657
- this.sessionCapabilities.delete(sessionId);
658
- }
659
- return new dntShim.Response(null, { status: 200, headers: this.getCORSHeaders(requestOrigin) });
660
- }
661
-
662
- // Only POST allowed for JSON-RPC messages
663
- if (request.method !== "POST") {
664
- return new dntShim.Response("Method Not Allowed", { status: 405 });
665
- }
666
-
667
- // Enforce request body size limit (fast path via Content-Length header)
668
- const contentLength = request.headers.get("content-length");
669
- if (contentLength && Number(contentLength) > MAX_REQUEST_BODY_SIZE) {
670
- return createJSONRPCErrorResponse(413, -32600, "Request body too large");
671
- }
672
-
673
- try {
674
- validateContentType(request, JSON_CONTENT_TYPE);
675
- } catch (error) {
676
- const message = error instanceof VeryfrontError ? error.message : "Invalid Content-Type";
677
- return createJSONRPCErrorResponse(400, -32700, message);
678
- }
679
-
680
- let rpcRequest: JSONRPCRequest;
681
- try {
682
- const bodyText = await request.text();
683
- if (bodyText.length > MAX_REQUEST_BODY_SIZE) {
684
- return createJSONRPCErrorResponse(413, -32600, "Request body too large");
685
- }
686
- rpcRequest = JSON.parse(bodyText) as JSONRPCRequest;
687
- } catch (_) {
688
- // expected: malformed JSON in request body
689
- return createJSONRPCErrorResponse(400, -32700, "Parse error");
690
- }
691
-
692
- // Session management: initialize creates session, everything else requires it
693
- const responseHeaders: Record<string, string> = {
694
- ...this.getCORSHeaders(requestOrigin),
695
- };
696
-
697
- if (rpcRequest.method === "initialize") {
698
- const context = this.extractRequestContext(request);
699
- const rpcResponse = await this.handleRequest(rpcRequest, context);
700
- const clientCaps =
701
- toParamsRecord(rpcRequest.params).capabilities as Record<string, unknown> ??
702
- {};
703
- const sessionId = this.sessionManager.create();
704
- this.sessionCapabilities.set(sessionId, clientCaps);
705
- responseHeaders["MCP-Session-Id"] = sessionId;
706
- return createJSONResponse(rpcResponse, { headers: responseHeaders });
707
- }
708
-
709
- // Post-init: require session ID when sessions are active
710
- if (this.sessionManager.size > 0) {
711
- const sessionId = request.headers.get("MCP-Session-Id");
712
- if (!sessionId) {
713
- return createJSONRPCErrorResponse(400, -32600, "Missing MCP-Session-Id header");
714
- }
715
- if (!this.sessionManager.isValid(sessionId)) {
716
- return createJSONRPCErrorResponse(404, -32600, "Session not found or expired");
717
- }
718
- }
719
-
720
- // Notifications have no id member — return 202 Accepted
721
- // Note: id:0 is a valid request ID per JSON-RPC 2.0, so check for undefined
722
- if (rpcRequest.id === undefined) {
723
- const context = this.extractRequestContext(request);
724
- await this.handleRequest(rpcRequest, context);
725
- return new dntShim.Response(null, { status: 202, headers: responseHeaders });
726
- }
727
-
728
- const context = this.extractRequestContext(request);
729
- const rpcResponse = await this.handleRequest(rpcRequest, context);
730
- return createJSONResponse(rpcResponse, { headers: responseHeaders });
731
- };
610
+ return createMCPHTTPHandler({
611
+ authEnabled: Boolean(this.config.auth?.type && this.config.auth.type !== "none"),
612
+ getCORSHeaders: (requestOrigin) => this.getCORSHeaders(requestOrigin),
613
+ validateAuth: (request) => this.validateAuth(request),
614
+ handleRequest: (request, context) => this.handleRequest(request, context),
615
+ extractRequestContext: (request) => this.extractRequestContext(request),
616
+ isOriginAllowed: (requestOrigin) =>
617
+ !requestOrigin ||
618
+ !this.config.cors?.enabled ||
619
+ !this.config.cors.origins?.length ||
620
+ this.config.cors.origins.includes(requestOrigin),
621
+ sessionCapabilities: this.sessionCapabilities,
622
+ sessionManager: this.sessionManager,
623
+ });
732
624
  }
733
625
 
734
626
  private extractRequestContext(request: dntShim.Request): ToolExecutionContext | undefined {
@@ -1,7 +1,7 @@
1
1
  import type { VeryfrontApiClient } from "../../veryfront-api-client/index.js";
2
2
  import { FileCache } from "../cache/file-cache.js";
3
+ import type { ContentContextProvider } from "./file-list-access.js";
3
4
  import { PathNormalizer } from "./path-normalizer.js";
4
- import type { ContentContextProvider } from "./read-operations.js";
5
5
 
6
6
  export class VeryfrontOperationsBase {
7
7
  constructor(
@@ -2,13 +2,9 @@ import { logger as baseLogger } from "../../../../utils/index.js";
2
2
  import type { DirectoryEntry } from "./types.js";
3
3
  import type { ProjectFile } from "../../veryfront-api-client/index.js";
4
4
  import { VeryfrontOperationsBase } from "./base-operations.js";
5
- import {
6
- buildDirCacheKeyPrefix,
7
- buildFileCacheKeyPrefix,
8
- buildFileListCacheKey,
9
- } from "./cache-keys.js";
5
+ import { buildDirCacheKeyPrefix } from "./cache-keys.js";
6
+ import { loadAllProjectFiles } from "./file-list-access.js";
10
7
  import { withSpan } from "../../../../observability/tracing/otlp-setup.js";
11
- import { withRetryOnTransient } from "./retry.js";
12
8
 
13
9
  const logger = baseLogger.component("directory-operations");
14
10
 
@@ -154,74 +150,13 @@ export class DirectoryOperations extends VeryfrontOperationsBase {
154
150
  }
155
151
 
156
152
  private getAllFilesRaw(): Promise<ProjectFile[]> {
157
- return withSpan("fs.veryfront.getAllFilesRaw", async () => {
158
- const cacheStart = performance.now();
159
- const ctx = this.contextProvider?.getContentContext();
160
- const cacheKeyPrefix = buildFileCacheKeyPrefix(ctx);
161
- const skipPersistentCache =
162
- this.contextProvider?.isPersistentCacheInvalidated?.(cacheKeyPrefix) ?? false;
163
-
164
- const adapterFiles = !skipPersistentCache
165
- ? await this.contextProvider?.getFileList?.()
166
- : undefined;
167
-
168
- if (adapterFiles) {
169
- const cacheMs = Math.round(performance.now() - cacheStart);
170
- logger.debug("getAllFilesRaw - from adapter cache", {
171
- cacheMs,
172
- fileCount: adapterFiles.length,
173
- });
174
- return adapterFiles as ProjectFile[];
175
- }
176
-
177
- const cacheKey = buildFileListCacheKey(ctx);
178
-
179
- if (skipPersistentCache) {
180
- logger.debug("getAllFilesRaw - skipping persistent cache", {
181
- cacheKey,
182
- cacheKeyPrefix,
183
- });
184
- }
185
-
186
- const cached = skipPersistentCache
187
- ? undefined
188
- : await this.cache.getAsync<ProjectFile[]>(cacheKey);
189
-
190
- const cacheMs = Math.round(performance.now() - cacheStart);
191
- if (cached) {
192
- logger.debug("getAllFilesRaw - fallback cache HIT", {
193
- cacheKey,
194
- cacheMs,
195
- fileCount: cached.length,
196
- });
197
- return cached;
198
- }
199
-
200
- logger.warn("getAllFilesRaw - cache MISS, fetching from API", {
201
- cacheKey,
202
- cacheMs,
203
- });
204
-
205
- const isPublished = ctx?.sourceType !== "branch";
206
- logger.debug("Fetching files from API", {
207
- sourceType: ctx?.sourceType,
208
- cacheKey,
209
- });
210
-
211
- const files = await withRetryOnTransient(
212
- () =>
213
- isPublished
214
- ? this.client.listPublishedFiles(
215
- undefined,
216
- ctx?.releaseId ?? undefined,
217
- ctx?.environmentName ?? undefined,
218
- )
219
- : this.client.listAllFiles(),
220
- "getAllFilesRaw (dir)",
221
- );
222
-
223
- this.cache.set(cacheKey, files);
224
- return files;
225
- });
153
+ return withSpan("fs.veryfront.getAllFilesRaw", () =>
154
+ loadAllProjectFiles({
155
+ client: this.client,
156
+ cache: this.cache,
157
+ contextProvider: this.contextProvider,
158
+ logger,
159
+ operationLabel: "dir",
160
+ }));
226
161
  }
227
162
  }
@@ -0,0 +1,109 @@
1
+ import type { ProjectFile, VeryfrontApiClient } from "../../veryfront-api-client/index.js";
2
+ import { FileCache } from "../cache/file-cache.js";
3
+ import { buildFileCacheKeyPrefix, buildFileListCacheKey } from "./cache-keys.js";
4
+ import { withRetryOnTransient } from "./retry.js";
5
+ import type { ResolvedContentContext } from "./types.js";
6
+
7
+ export interface ContentContextProvider {
8
+ isProductionMode: () => boolean;
9
+ getReleaseId: () => string | null;
10
+ getContentContext: () => ResolvedContentContext | null;
11
+ getFileList?: () => Promise<
12
+ Array<{
13
+ id?: string;
14
+ path: string;
15
+ content?: string;
16
+ type?: string;
17
+ size?: number;
18
+ updated_at?: string;
19
+ }> | undefined
20
+ >;
21
+ hasCachedFileList?: () => Promise<boolean>;
22
+ isPersistentCacheInvalidated?: (prefix: string) => boolean;
23
+ isReleaseBeingInvalidated?: (releaseId: string) => boolean;
24
+ }
25
+
26
+ interface FileListLogger {
27
+ debug(message: string, context?: Record<string, unknown>): void;
28
+ warn(message: string, context?: Record<string, unknown>): void;
29
+ }
30
+
31
+ interface LoadAllProjectFilesOptions {
32
+ client: VeryfrontApiClient;
33
+ cache: FileCache;
34
+ contextProvider?: ContentContextProvider;
35
+ logger: FileListLogger;
36
+ operationLabel: string;
37
+ }
38
+
39
+ export async function loadAllProjectFiles({
40
+ client,
41
+ cache,
42
+ contextProvider,
43
+ logger,
44
+ operationLabel,
45
+ }: LoadAllProjectFilesOptions): Promise<ProjectFile[]> {
46
+ const cacheStart = performance.now();
47
+ const ctx = contextProvider?.getContentContext();
48
+ const cacheKeyPrefix = buildFileCacheKeyPrefix(ctx);
49
+ const skipPersistentCache = contextProvider?.isPersistentCacheInvalidated?.(cacheKeyPrefix) ??
50
+ false;
51
+
52
+ const adapterFiles = !skipPersistentCache ? await contextProvider?.getFileList?.() : undefined;
53
+
54
+ if (adapterFiles) {
55
+ const cacheMs = Math.round(performance.now() - cacheStart);
56
+ logger.debug("getAllFilesRaw - from adapter cache", {
57
+ cacheMs,
58
+ fileCount: adapterFiles.length,
59
+ });
60
+ return adapterFiles as ProjectFile[];
61
+ }
62
+
63
+ const cacheKey = buildFileListCacheKey(ctx);
64
+
65
+ if (skipPersistentCache) {
66
+ logger.debug("getAllFilesRaw - skipping persistent cache", {
67
+ cacheKey,
68
+ cacheKeyPrefix,
69
+ });
70
+ }
71
+
72
+ const cached = skipPersistentCache ? undefined : await cache.getAsync<ProjectFile[]>(cacheKey);
73
+ const cacheMs = Math.round(performance.now() - cacheStart);
74
+
75
+ if (cached) {
76
+ logger.debug("getAllFilesRaw - fallback cache HIT", {
77
+ cacheKey,
78
+ cacheMs,
79
+ fileCount: cached.length,
80
+ });
81
+ return cached;
82
+ }
83
+
84
+ logger.warn("getAllFilesRaw - cache MISS, fetching from API", {
85
+ cacheKey,
86
+ cacheMs,
87
+ });
88
+
89
+ const isPublished = ctx?.sourceType !== "branch";
90
+ logger.debug("Fetching files from API", {
91
+ sourceType: ctx?.sourceType,
92
+ cacheKey,
93
+ });
94
+
95
+ const files = await withRetryOnTransient(
96
+ () =>
97
+ isPublished
98
+ ? client.listPublishedFiles(
99
+ undefined,
100
+ ctx?.releaseId ?? undefined,
101
+ ctx?.environmentName ?? undefined,
102
+ )
103
+ : client.listAllFiles(),
104
+ `getAllFilesRaw (${operationLabel})`,
105
+ );
106
+
107
+ cache.set(cacheKey, files);
108
+ return files;
109
+ }
@@ -7,6 +7,7 @@ import { FileListIndex, type FileListMatchResult } from "./file-list-index.js";
7
7
  import { InFlightRequestDeduper } from "./in-flight-dedupe.js";
8
8
  import { getRequestScopedFile, setRequestScopedFile } from "./multi-project-adapter.js";
9
9
  import { PathNormalizer } from "./path-normalizer.js";
10
+ import type { ContentContextProvider } from "./file-list-access.js";
10
11
  import {
11
12
  assertProjectSourcePath,
12
13
  buildExtensionCandidatePaths,
@@ -28,28 +29,6 @@ export {
28
29
 
29
30
  const logger = baseLogger.component("read-operations");
30
31
 
31
- export interface ContentContextProvider {
32
- isProductionMode: () => boolean;
33
- getReleaseId: () => string | null;
34
- getContentContext: () => ResolvedContentContext | null;
35
- /** Cached file list from adapter initialization (single source of truth) */
36
- getFileList?: () => Promise<
37
- Array<{
38
- id?: string;
39
- path: string;
40
- content?: string;
41
- type?: string;
42
- size?: number;
43
- updated_at?: string;
44
- }> | undefined
45
- >;
46
- hasCachedFileList?: () => Promise<boolean>;
47
- /** True if cache prefix is being deleted - skip persistent cache reads */
48
- isPersistentCacheInvalidated?: (prefix: string) => boolean;
49
- /** Back-compat: release-scoped invalidation */
50
- isReleaseBeingInvalidated?: (releaseId: string) => boolean;
51
- }
52
-
53
32
  const IN_FLIGHT_REQUEST_TIMEOUT_MS = 15_000;
54
33
  const MAX_IN_FLIGHT_REQUESTS = 100;
55
34
  const IN_FLIGHT_CLEANUP_INTERVAL_MS = 1_000;
@@ -4,12 +4,7 @@ import type { FileInfo, ResolveFileOptions } from "../../base.js";
4
4
  import type { ProjectFile } from "../../veryfront-api-client/index.js";
5
5
  import { VeryfrontOperationsBase } from "./base-operations.js";
6
6
  import { createError, toError } from "../../../../errors/index.js";
7
- import {
8
- buildFileCacheKeyPrefix,
9
- buildFileListCacheKey,
10
- buildStatCacheKeyPrefix,
11
- } from "./cache-keys.js";
12
- import { withRetryOnTransient } from "./retry.js";
7
+ import { buildStatCacheKeyPrefix } from "./cache-keys.js";
13
8
  import { STAT_OPERATION_EXTENSION_PRIORITY as EXTENSION_PRIORITY } from "./extension-priority.js";
14
9
  import {
15
10
  collectParentDirectories,
@@ -21,6 +16,7 @@ import {
21
16
  } from "./stat-operations-helpers.js";
22
17
  import { ApiSearchCircuitBreaker } from "./api-search-circuit-breaker.js";
23
18
  import { withSpan } from "../../../../observability/tracing/otlp-setup.js";
19
+ import { loadAllProjectFiles } from "./file-list-access.js";
24
20
 
25
21
  const logger = baseLogger.component("stat-operations");
26
22
 
@@ -258,77 +254,19 @@ export class StatOperations extends VeryfrontOperationsBase {
258
254
  }
259
255
 
260
256
  private async getAllFilesRaw(): Promise<ProjectFile[]> {
261
- const cacheStart = performance.now();
262
- const ctx = this.contextProvider?.getContentContext();
263
- const cacheKeyPrefix = buildFileCacheKeyPrefix(ctx);
264
- const skipPersistentCache =
265
- this.contextProvider?.isPersistentCacheInvalidated?.(cacheKeyPrefix) ?? false;
266
-
267
- if (!skipPersistentCache) {
268
- const files = await this.contextProvider?.getFileList?.();
269
- if (files) {
270
- const cacheMs = Math.round(performance.now() - cacheStart);
271
- logger.debug("getAllFilesRaw - from adapter cache", {
272
- cacheMs,
273
- fileCount: files.length,
274
- });
275
- return files as ProjectFile[];
276
- }
277
- }
278
-
279
- const cacheKey = buildFileListCacheKey(ctx);
280
-
281
- if (skipPersistentCache) {
282
- logger.debug("getAllFilesRaw - skipping persistent cache (invalidation)", {
283
- cacheKey,
284
- cacheKeyPrefix,
285
- });
286
- }
287
-
288
- const cached = skipPersistentCache
289
- ? undefined
290
- : await this.cache.getAsync<ProjectFile[]>(cacheKey);
291
- const cacheMs = Math.round(performance.now() - cacheStart);
292
-
293
- if (cached) {
294
- logger.debug("getAllFilesRaw - fallback cache HIT", {
295
- cacheKey,
296
- cacheMs,
297
- fileCount: cached.length,
298
- });
299
- return cached;
300
- }
301
-
302
- logger.warn("getAllFilesRaw - cache MISS, fetching from API", {
303
- cacheKey,
304
- cacheMs,
257
+ return await loadAllProjectFiles({
258
+ client: this.client,
259
+ cache: this.cache,
260
+ contextProvider: this.contextProvider,
261
+ logger,
262
+ operationLabel: "stat",
305
263
  });
306
-
307
- const isPublished = ctx?.sourceType !== "branch";
308
- logger.debug("Fetching files from API", {
309
- sourceType: ctx?.sourceType,
310
- cacheKey,
311
- });
312
-
313
- const files = await withRetryOnTransient(
314
- () =>
315
- isPublished
316
- ? this.client.listPublishedFiles(
317
- undefined,
318
- ctx?.releaseId ?? undefined,
319
- ctx?.environmentName ?? undefined,
320
- )
321
- : this.client.listAllFiles(),
322
- "getAllFilesRaw (stat)",
323
- );
324
-
325
- this.cache.set(cacheKey, files);
326
- return files;
327
264
  }
328
265
 
329
266
  private buildResolveSearchPatterns(
330
267
  normalizedPath: string,
331
268
  options?: ResolveFileOptions,
269
+ knownExtensionFallback: "exact" | "wildcard" = "exact",
332
270
  ): string[] {
333
271
  const patterns = new Set<string>();
334
272
  const pathWithoutExt = stripKnownExtension(normalizedPath, EXTENSION_PRIORITY);
@@ -338,7 +276,9 @@ export class StatOperations extends VeryfrontOperationsBase {
338
276
  };
339
277
 
340
278
  if (EXTENSION_PRIORITY.some((ext) => normalizedPath.endsWith(ext))) {
341
- addPattern(normalizedPath);
279
+ addPattern(
280
+ knownExtensionFallback === "wildcard" ? `${pathWithoutExt}.*` : normalizedPath,
281
+ );
342
282
  return [...patterns];
343
283
  }
344
284
 
@@ -366,6 +306,7 @@ export class StatOperations extends VeryfrontOperationsBase {
366
306
  private async tryResolveViaApiSearch(
367
307
  normalizedPath: string,
368
308
  options?: ResolveFileOptions,
309
+ knownExtensionFallback: "exact" | "wildcard" = "exact",
369
310
  ): Promise<string | null | undefined> {
370
311
  if (isFrameworkSourcePath(normalizedPath)) {
371
312
  logger.debug("Skipping API search for framework path", { normalizedPath });
@@ -377,7 +318,11 @@ export class StatOperations extends VeryfrontOperationsBase {
377
318
  return undefined;
378
319
  }
379
320
 
380
- const patterns = this.buildResolveSearchPatterns(normalizedPath, options);
321
+ const patterns = this.buildResolveSearchPatterns(
322
+ normalizedPath,
323
+ options,
324
+ knownExtensionFallback,
325
+ );
381
326
  let sawSuccessfulSearch = false;
382
327
 
383
328
  for (const pattern of patterns) {
@@ -573,55 +518,17 @@ export class StatOperations extends VeryfrontOperationsBase {
573
518
  return null;
574
519
  }
575
520
 
576
- // NOTE: Removed optimization that skipped API search for paths with extensions.
577
- // This was causing layout files and other project files to not be found when
578
- // they were missing from the file index (due to cache issues, incomplete fetch, etc.).
579
- // The API pattern search is the fallback to ensure files can still be found.
580
-
581
- if (!this.apiSearchCircuitBreaker.canSearch()) {
582
- logger.warn("API search circuit breaker open, skipping", { normalizedPath });
583
- return null;
521
+ // NOTE: Keep the post-index API fallback aligned with the pre-index helper for extensionless
522
+ // paths, while preserving the older wildcard sibling-extension lookup for known-extension
523
+ // paths. Incomplete file-list snapshots otherwise hide valid files until the cache refreshes.
524
+ const apiResolved = await this.tryResolveViaApiSearch(normalizedPath, options, "wildcard");
525
+ if (typeof apiResolved === "string") {
526
+ this.cache.set(cacheKey, apiResolved);
527
+ return apiResolved;
584
528
  }
585
-
586
- const searchPattern = `${pathWithoutExt}.*`;
587
- logger.debug("Searching for file via API", {
588
- pattern: searchPattern,
589
- normalizedPath,
590
- });
591
-
592
- try {
593
- const matches = await this.client.searchFiles(searchPattern);
594
- this.apiSearchCircuitBreaker.recordSuccess();
595
-
596
- logger.debug("API search result", {
597
- pattern: searchPattern,
598
- matchCount: matches.length,
599
- matches: matches.map((m) => m.path).slice(0, 5),
600
- });
601
-
602
- const sortedMatches = sortPathsByExtensionPriority(matches, EXTENSION_PRIORITY);
603
- const first = sortedMatches[0];
604
- if (first) {
605
- logger.debug("resolveFile found via API search", { path: first.path });
606
- this.cache.set(cacheKey, first.path);
607
- return first.path;
608
- }
609
- } catch (error) {
610
- const result = this.apiSearchCircuitBreaker.recordFailure();
611
- if (result.tripped) {
612
- logger.warn("API search circuit breaker tripped", {
613
- failures: result.failures,
614
- });
615
- }
616
- logger.error("API pattern search failed", { pattern: searchPattern, error });
529
+ if (apiResolved === null) {
530
+ this.cache.set(cacheKey, NOT_FOUND_SENTINEL);
617
531
  }
618
-
619
- logger.debug("resolveFile not found after API search", {
620
- normalizedPath,
621
- pathWithoutExt,
622
- });
623
-
624
- this.cache.set(cacheKey, NOT_FOUND_SENTINEL);
625
532
  return null;
626
533
  }
627
534
  }
@@ -179,20 +179,18 @@ export interface DynamicToolConfig {
179
179
  id?: string;
180
180
  description: string;
181
181
  inputSchema: unknown;
182
+ inputSchemaJson?: JsonSchema;
182
183
  execute: (input: unknown, context?: ToolExecutionContext) => Promise<unknown> | unknown;
183
184
  toModelOutput?: (output: unknown) => unknown;
184
- mcp?: {
185
- enabled?: boolean;
186
- requiresAuth?: boolean;
187
- cachePolicy?: "no-cache" | "cache" | "cache-first";
188
- };
185
+ mcp?: ToolConfig["mcp"];
189
186
  }
190
187
 
191
188
  export function dynamicTool(config: DynamicToolConfig): Tool<unknown, unknown> {
192
189
  const explicitId = typeof config.id === "string" && config.id.length > 0 ? config.id : undefined;
193
190
  const id = explicitId ?? generateToolId();
194
191
 
195
- const inputSchemaJson = convertSchemaToJson(config.inputSchema, id, "DYNAMIC_TOOL", true);
192
+ const inputSchemaJson = config.inputSchemaJson ??
193
+ convertSchemaToJson(config.inputSchema, id, "DYNAMIC_TOOL", true);
196
194
 
197
195
  const createdTool: Tool<unknown, unknown> = {
198
196
  id,
@@ -57,6 +57,11 @@ export { dynamicTool, tool } from "./factory.js";
57
57
  export type { DynamicToolConfig } from "./factory.js";
58
58
  export { createRemoteMCPToolSource } from "./remote-mcp.js";
59
59
  export type { RemoteMCPToolSourceConfig } from "./remote-mcp.js";
60
+ export {
61
+ createToolsFromRemoteDefinitions,
62
+ loadRemoteToolsFromSource,
63
+ } from "./remote-source-tools.js";
64
+ export type { RemoteToolMaterializationOptions } from "./remote-source-tools.js";
60
65
 
61
66
  export { toolRegistry } from "./registry.js";
62
67