@enactprotocol/shared 2.1.23 → 2.1.28
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/dist/execution/index.d.ts +1 -1
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/types.d.ts +15 -0
- package/dist/execution/types.d.ts.map +1 -1
- package/dist/execution/types.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/manifest/index.d.ts +2 -2
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/manifest/index.js +1 -1
- package/dist/manifest/index.js.map +1 -1
- package/dist/manifest/loader.d.ts +14 -4
- package/dist/manifest/loader.d.ts.map +1 -1
- package/dist/manifest/loader.js +12 -8
- package/dist/manifest/loader.js.map +1 -1
- package/dist/manifest/validator.d.ts +21 -4
- package/dist/manifest/validator.d.ts.map +1 -1
- package/dist/manifest/validator.js +72 -47
- package/dist/manifest/validator.js.map +1 -1
- package/dist/resolver.d.ts +29 -0
- package/dist/resolver.d.ts.map +1 -1
- package/dist/resolver.js +115 -17
- package/dist/resolver.js.map +1 -1
- package/package.json +2 -2
- package/src/execution/index.ts +1 -0
- package/src/execution/types.ts +16 -0
- package/src/index.ts +27 -0
- package/src/manifest/index.ts +3 -0
- package/src/manifest/loader.ts +29 -9
- package/src/manifest/validator.ts +102 -57
- package/src/mcp-registry.ts +337 -0
- package/src/resolver.ts +153 -18
- package/tests/manifest/validator.test.ts +77 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/resolver.ts
CHANGED
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
import { existsSync, readdirSync } from "node:fs";
|
|
12
12
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
type LoadManifestOptions,
|
|
15
|
+
ManifestLoadError,
|
|
16
|
+
findManifestFile,
|
|
17
|
+
loadManifest,
|
|
18
|
+
} from "./manifest/loader";
|
|
14
19
|
import { getCacheDir, getProjectEnactDir } from "./paths";
|
|
15
20
|
import { getInstalledVersion, getToolCachePath } from "./registry";
|
|
16
21
|
import type { ToolLocation, ToolResolution } from "./types/manifest";
|
|
@@ -29,6 +34,22 @@ export class ToolResolveError extends Error {
|
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Result of trying to resolve a tool with error details
|
|
39
|
+
*/
|
|
40
|
+
export interface TryResolveResult {
|
|
41
|
+
/** The resolved tool, or null if not found/invalid */
|
|
42
|
+
resolution: ToolResolution | null;
|
|
43
|
+
/** Error that occurred during resolution, if any */
|
|
44
|
+
error?: Error;
|
|
45
|
+
/** Locations that were searched */
|
|
46
|
+
searchedLocations: string[];
|
|
47
|
+
/** Whether a manifest was found but had errors */
|
|
48
|
+
manifestFound: boolean;
|
|
49
|
+
/** Path where manifest was found (if any) */
|
|
50
|
+
manifestPath?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
32
53
|
/**
|
|
33
54
|
* Options for tool resolution
|
|
34
55
|
*/
|
|
@@ -73,9 +94,14 @@ export function getToolPath(toolsDir: string, toolName: string): string {
|
|
|
73
94
|
*
|
|
74
95
|
* @param dir - Directory to check
|
|
75
96
|
* @param location - The location type for metadata
|
|
97
|
+
* @param options - Options for loading the manifest
|
|
76
98
|
* @returns ToolResolution or null if not found/invalid
|
|
77
99
|
*/
|
|
78
|
-
function tryLoadFromDir(
|
|
100
|
+
function tryLoadFromDir(
|
|
101
|
+
dir: string,
|
|
102
|
+
location: ToolLocation,
|
|
103
|
+
options: LoadManifestOptions = {}
|
|
104
|
+
): ToolResolution | null {
|
|
79
105
|
if (!existsSync(dir)) {
|
|
80
106
|
return null;
|
|
81
107
|
}
|
|
@@ -86,7 +112,7 @@ function tryLoadFromDir(dir: string, location: ToolLocation): ToolResolution | n
|
|
|
86
112
|
}
|
|
87
113
|
|
|
88
114
|
try {
|
|
89
|
-
const loaded = loadManifest(manifestPath);
|
|
115
|
+
const loaded = loadManifest(manifestPath, options);
|
|
90
116
|
return {
|
|
91
117
|
manifest: loaded.manifest,
|
|
92
118
|
sourceDir: dir,
|
|
@@ -103,6 +129,9 @@ function tryLoadFromDir(dir: string, location: ToolLocation): ToolResolution | n
|
|
|
103
129
|
/**
|
|
104
130
|
* Resolve a tool from a file path
|
|
105
131
|
*
|
|
132
|
+
* Local/file tools are allowed to have simple names (without hierarchy)
|
|
133
|
+
* since they don't need to be published.
|
|
134
|
+
*
|
|
106
135
|
* @param filePath - Path to manifest file or directory containing manifest
|
|
107
136
|
* @returns ToolResolution
|
|
108
137
|
* @throws ToolResolveError if not found
|
|
@@ -110,6 +139,9 @@ function tryLoadFromDir(dir: string, location: ToolLocation): ToolResolution | n
|
|
|
110
139
|
export function resolveToolFromPath(filePath: string): ToolResolution {
|
|
111
140
|
const absolutePath = isAbsolute(filePath) ? filePath : resolve(filePath);
|
|
112
141
|
|
|
142
|
+
// Local tools can have simple names (no hierarchy required)
|
|
143
|
+
const localOptions: LoadManifestOptions = { allowSimpleNames: true };
|
|
144
|
+
|
|
113
145
|
// Check if it's a manifest file directly
|
|
114
146
|
if (
|
|
115
147
|
absolutePath.endsWith(".yaml") ||
|
|
@@ -120,7 +152,7 @@ export function resolveToolFromPath(filePath: string): ToolResolution {
|
|
|
120
152
|
throw new ToolResolveError(`Manifest file not found: ${absolutePath}`, filePath);
|
|
121
153
|
}
|
|
122
154
|
|
|
123
|
-
const loaded = loadManifest(absolutePath);
|
|
155
|
+
const loaded = loadManifest(absolutePath, localOptions);
|
|
124
156
|
return {
|
|
125
157
|
manifest: loaded.manifest,
|
|
126
158
|
sourceDir: dirname(absolutePath),
|
|
@@ -131,7 +163,7 @@ export function resolveToolFromPath(filePath: string): ToolResolution {
|
|
|
131
163
|
}
|
|
132
164
|
|
|
133
165
|
// Treat as directory
|
|
134
|
-
const result = tryLoadFromDir(absolutePath, "file");
|
|
166
|
+
const result = tryLoadFromDir(absolutePath, "file", localOptions);
|
|
135
167
|
if (result) {
|
|
136
168
|
return result;
|
|
137
169
|
}
|
|
@@ -256,21 +288,123 @@ export function tryResolveTool(
|
|
|
256
288
|
toolNameOrPath: string,
|
|
257
289
|
options: ResolveOptions = {}
|
|
258
290
|
): ToolResolution | null {
|
|
291
|
+
const result = tryResolveToolDetailed(toolNameOrPath, options);
|
|
292
|
+
return result.resolution;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Try to resolve a tool with detailed error information
|
|
297
|
+
*
|
|
298
|
+
* Unlike tryResolveTool, this function returns information about why
|
|
299
|
+
* resolution failed, allowing callers to provide better error messages.
|
|
300
|
+
*
|
|
301
|
+
* @param toolNameOrPath - Tool name or path
|
|
302
|
+
* @param options - Resolution options
|
|
303
|
+
* @returns TryResolveResult with resolution or error details
|
|
304
|
+
*/
|
|
305
|
+
export function tryResolveToolDetailed(
|
|
306
|
+
toolNameOrPath: string,
|
|
307
|
+
options: ResolveOptions = {}
|
|
308
|
+
): TryResolveResult {
|
|
309
|
+
const searchedLocations: string[] = [];
|
|
310
|
+
|
|
311
|
+
// Check if it looks like a path
|
|
312
|
+
const isPath =
|
|
313
|
+
toolNameOrPath.startsWith("/") ||
|
|
314
|
+
toolNameOrPath.startsWith("./") ||
|
|
315
|
+
toolNameOrPath.startsWith("../") ||
|
|
316
|
+
toolNameOrPath.includes("\\") ||
|
|
317
|
+
existsSync(toolNameOrPath);
|
|
318
|
+
|
|
319
|
+
if (isPath) {
|
|
320
|
+
// Resolve from path
|
|
321
|
+
const absolutePath = isAbsolute(toolNameOrPath) ? toolNameOrPath : resolve(toolNameOrPath);
|
|
322
|
+
searchedLocations.push(absolutePath);
|
|
323
|
+
|
|
324
|
+
// Check if path exists
|
|
325
|
+
if (!existsSync(absolutePath)) {
|
|
326
|
+
return {
|
|
327
|
+
resolution: null,
|
|
328
|
+
searchedLocations,
|
|
329
|
+
manifestFound: false,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Find manifest file
|
|
334
|
+
const manifestPath =
|
|
335
|
+
absolutePath.endsWith(".yaml") ||
|
|
336
|
+
absolutePath.endsWith(".yml") ||
|
|
337
|
+
absolutePath.endsWith(".md")
|
|
338
|
+
? absolutePath
|
|
339
|
+
: findManifestFile(absolutePath);
|
|
340
|
+
|
|
341
|
+
if (!manifestPath) {
|
|
342
|
+
return {
|
|
343
|
+
resolution: null,
|
|
344
|
+
searchedLocations,
|
|
345
|
+
manifestFound: false,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Try to load the manifest
|
|
350
|
+
try {
|
|
351
|
+
const resolution = resolveToolFromPath(toolNameOrPath);
|
|
352
|
+
return {
|
|
353
|
+
resolution,
|
|
354
|
+
searchedLocations,
|
|
355
|
+
manifestFound: true,
|
|
356
|
+
manifestPath,
|
|
357
|
+
};
|
|
358
|
+
} catch (error) {
|
|
359
|
+
// Manifest found but invalid
|
|
360
|
+
return {
|
|
361
|
+
resolution: null,
|
|
362
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
363
|
+
searchedLocations,
|
|
364
|
+
manifestFound: true,
|
|
365
|
+
manifestPath,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Resolve by name
|
|
259
371
|
try {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
toolNameOrPath
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
372
|
+
const resolution = resolveTool(toolNameOrPath, options);
|
|
373
|
+
return {
|
|
374
|
+
resolution,
|
|
375
|
+
searchedLocations: getToolSearchPaths(toolNameOrPath, options),
|
|
376
|
+
manifestFound: true,
|
|
377
|
+
manifestPath: resolution.manifestPath,
|
|
378
|
+
};
|
|
379
|
+
} catch (error) {
|
|
380
|
+
// Check if error is due to manifest validation vs not found
|
|
381
|
+
if (error instanceof ToolResolveError) {
|
|
382
|
+
return {
|
|
383
|
+
resolution: null,
|
|
384
|
+
error,
|
|
385
|
+
searchedLocations: error.searchedLocations ?? [],
|
|
386
|
+
manifestFound: false,
|
|
387
|
+
};
|
|
269
388
|
}
|
|
270
389
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
390
|
+
// ManifestLoadError means manifest was found but invalid
|
|
391
|
+
if (error instanceof ManifestLoadError) {
|
|
392
|
+
return {
|
|
393
|
+
resolution: null,
|
|
394
|
+
error,
|
|
395
|
+
searchedLocations: getToolSearchPaths(toolNameOrPath, options),
|
|
396
|
+
manifestFound: true,
|
|
397
|
+
manifestPath: error.filePath,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Other error
|
|
402
|
+
return {
|
|
403
|
+
resolution: null,
|
|
404
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
405
|
+
searchedLocations: getToolSearchPaths(toolNameOrPath, options),
|
|
406
|
+
manifestFound: false,
|
|
407
|
+
};
|
|
274
408
|
}
|
|
275
409
|
}
|
|
276
410
|
|
|
@@ -298,7 +432,8 @@ export function resolveToolAuto(
|
|
|
298
432
|
|
|
299
433
|
// Check if the path exists as-is (could be a relative directory without ./)
|
|
300
434
|
if (existsSync(toolNameOrPath)) {
|
|
301
|
-
|
|
435
|
+
// Local tools can have simple names (no hierarchy required)
|
|
436
|
+
const result = tryLoadFromDir(resolve(toolNameOrPath), "file", { allowSimpleNames: true });
|
|
302
437
|
if (result) {
|
|
303
438
|
return result;
|
|
304
439
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
+
isValidLocalToolName,
|
|
3
4
|
isValidTimeout,
|
|
4
5
|
isValidToolName,
|
|
5
6
|
isValidVersion,
|
|
@@ -237,6 +238,63 @@ describe("manifest validator", () => {
|
|
|
237
238
|
});
|
|
238
239
|
});
|
|
239
240
|
|
|
241
|
+
describe("allowSimpleNames option", () => {
|
|
242
|
+
test("rejects simple names by default", () => {
|
|
243
|
+
const manifest = {
|
|
244
|
+
name: "my-tool", // No slash
|
|
245
|
+
description: "A local tool",
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const result = validateManifest(manifest);
|
|
249
|
+
expect(result.valid).toBe(false);
|
|
250
|
+
expect(result.errors?.some((e) => e.path === "name")).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("accepts simple names with allowSimpleNames option", () => {
|
|
254
|
+
const manifest = {
|
|
255
|
+
name: "my-tool",
|
|
256
|
+
description: "A local tool",
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const result = validateManifest(manifest, { allowSimpleNames: true });
|
|
260
|
+
expect(result.valid).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("accepts hierarchical names with allowSimpleNames option", () => {
|
|
264
|
+
const manifest = {
|
|
265
|
+
name: "org/tool",
|
|
266
|
+
description: "A published tool",
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const result = validateManifest(manifest, { allowSimpleNames: true });
|
|
270
|
+
expect(result.valid).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("still rejects invalid characters with allowSimpleNames", () => {
|
|
274
|
+
const manifest = {
|
|
275
|
+
name: "My-Tool", // Uppercase not allowed
|
|
276
|
+
description: "Invalid tool name",
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const result = validateManifest(manifest, { allowSimpleNames: true });
|
|
280
|
+
expect(result.valid).toBe(false);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("validateManifestStrict respects allowSimpleNames option", () => {
|
|
284
|
+
const manifest = {
|
|
285
|
+
name: "local-tool",
|
|
286
|
+
description: "A local tool",
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// Should throw without option
|
|
290
|
+
expect(() => validateManifestStrict(manifest)).toThrow();
|
|
291
|
+
|
|
292
|
+
// Should succeed with option
|
|
293
|
+
const result = validateManifestStrict(manifest, { allowSimpleNames: true });
|
|
294
|
+
expect(result.name).toBe("local-tool");
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
240
298
|
describe("warnings", () => {
|
|
241
299
|
test("warns about missing recommended fields", () => {
|
|
242
300
|
const manifest = {
|
|
@@ -360,6 +418,25 @@ describe("manifest validator", () => {
|
|
|
360
418
|
});
|
|
361
419
|
});
|
|
362
420
|
|
|
421
|
+
describe("isValidLocalToolName", () => {
|
|
422
|
+
test("returns true for simple names (no hierarchy)", () => {
|
|
423
|
+
expect(isValidLocalToolName("my-tool")).toBe(true);
|
|
424
|
+
expect(isValidLocalToolName("tool_name")).toBe(true);
|
|
425
|
+
expect(isValidLocalToolName("simple")).toBe(true);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("returns true for hierarchical names", () => {
|
|
429
|
+
expect(isValidLocalToolName("org/tool")).toBe(true);
|
|
430
|
+
expect(isValidLocalToolName("acme/utils/greeter")).toBe(true);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("returns false for invalid names", () => {
|
|
434
|
+
expect(isValidLocalToolName("My-Tool")).toBe(false); // Uppercase
|
|
435
|
+
expect(isValidLocalToolName("tool name")).toBe(false); // Space
|
|
436
|
+
expect(isValidLocalToolName("")).toBe(false);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
363
440
|
describe("isValidVersion", () => {
|
|
364
441
|
test("returns true for valid semver", () => {
|
|
365
442
|
expect(isValidVersion("1.0.0")).toBe(true);
|