@valbuild/mcp 0.123.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/CHANGELOG.md +109 -0
- package/README.md +79 -0
- package/dist/asyncToGenerator-500f022f.esm.js +137 -0
- package/dist/asyncToGenerator-8e5c36c8.cjs.prod.js +140 -0
- package/dist/asyncToGenerator-c3823d62.cjs.dev.js +140 -0
- package/dist/declarations/src/images/imageTools.d.ts +14 -0
- package/dist/declarations/src/images/index.d.ts +2 -0
- package/dist/declarations/src/images/remoteUploadTarget.d.ts +70 -0
- package/dist/declarations/src/images/types.d.ts +60 -0
- package/dist/declarations/src/index.d.ts +24 -0
- package/dist/declarations/src/initValMcp.d.ts +95 -0
- package/dist/declarations/src/sharp/index.d.ts +46 -0
- package/dist/declarations/src/tools/createValTools.d.ts +47 -0
- package/dist/declarations/src/tools/defineTool.d.ts +79 -0
- package/dist/declarations/src/tools/index.d.ts +7 -0
- package/dist/declarations/src/tools/types.d.ts +172 -0
- package/dist/declarations/src/tools/writePath.d.ts +160 -0
- package/dist/declarations/src/valAccessToken.d.ts +93 -0
- package/dist/declarations/src/valMcpMetadata.d.ts +47 -0
- package/dist/valbuild-mcp.cjs.d.ts +2 -0
- package/dist/valbuild-mcp.cjs.dev.js +4399 -0
- package/dist/valbuild-mcp.cjs.js +7 -0
- package/dist/valbuild-mcp.cjs.prod.js +4399 -0
- package/dist/valbuild-mcp.esm.js +4381 -0
- package/package.json +66 -0
- package/sharp/dist/valbuild-mcp-sharp.cjs.d.ts +2 -0
- package/sharp/dist/valbuild-mcp-sharp.cjs.dev.js +157 -0
- package/sharp/dist/valbuild-mcp-sharp.cjs.js +7 -0
- package/sharp/dist/valbuild-mcp-sharp.cjs.prod.js +157 -0
- package/sharp/dist/valbuild-mcp-sharp.esm.js +153 -0
- package/sharp/package.json +4 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type SerializedFileSchema, type SerializedImageSchema } from "@valbuild/core";
|
|
2
|
+
import { type ValServerConfig } from "@valbuild/server";
|
|
3
|
+
import { type ValToolError } from "../tools/index.js";
|
|
4
|
+
/**
|
|
5
|
+
* Where a remote image goes, and what its ref has to say.
|
|
6
|
+
*
|
|
7
|
+
* Only one thing here needs the network: a project's `publicProjectId` and its
|
|
8
|
+
* list of buckets, which `getSettings` answers. Everything else is arithmetic
|
|
9
|
+
* over bytes the caller already handed us.
|
|
10
|
+
*
|
|
11
|
+
* And **no credential comes from the MCP caller**, in either mode. The rule is
|
|
12
|
+
* `resolveRemoteFileAuth` in `@valbuild/server`, shared with the Studio's own
|
|
13
|
+
* api routes: the app's api key where there is one, and otherwise — local
|
|
14
|
+
* development — the developer's own `val login` token off disk. That is the
|
|
15
|
+
* same precondition `val validate --fix` has, and the same one the Studio has
|
|
16
|
+
* when it uploads a remote image from a laptop.
|
|
17
|
+
*
|
|
18
|
+
* What this does NOT do is upload anything to the content host. A remote
|
|
19
|
+
* image's bytes go into the patch store like any other pending file, and the
|
|
20
|
+
* push to `remote.val.build` happens at publish, from
|
|
21
|
+
* `ValOpsFS.saveOrUploadFiles(mode: "upload-remote")`. See
|
|
22
|
+
* `docs/plans/mcp-remote-images.md` Part A.
|
|
23
|
+
*/
|
|
24
|
+
export type RemoteUploadTarget = {
|
|
25
|
+
publicProjectId: string;
|
|
26
|
+
bucket: string;
|
|
27
|
+
coreVersion: string;
|
|
28
|
+
remoteHost: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Resolves the project's remote-file settings, once.
|
|
32
|
+
*
|
|
33
|
+
* Cached for the life of the process because the answer does not move — a
|
|
34
|
+
* project's public id never changes and its bucket list changes when someone
|
|
35
|
+
* adds a bucket, which is not something an agent's upload should pay a network
|
|
36
|
+
* round trip to notice. Held as the promise rather than the value so that two
|
|
37
|
+
* uploads arriving together make one request rather than two.
|
|
38
|
+
*/
|
|
39
|
+
export type RemoteSettingsLoader = () => Promise<{
|
|
40
|
+
status: "success";
|
|
41
|
+
publicProjectId: string;
|
|
42
|
+
buckets: string[];
|
|
43
|
+
} | ValToolError>;
|
|
44
|
+
export declare function createRemoteSettingsLoader(options: ValServerConfig): RemoteSettingsLoader;
|
|
45
|
+
export declare function nextBucket(buckets: string[]): string;
|
|
46
|
+
/** Test seam: the rotation is process-global, so a suite has to be able to reset it. */
|
|
47
|
+
export declare function resetBucketRotation(): void;
|
|
48
|
+
export declare function resolveRemoteUploadTarget(loadSettings: RemoteSettingsLoader): Promise<{
|
|
49
|
+
status: "success";
|
|
50
|
+
target: RemoteUploadTarget;
|
|
51
|
+
} | ValToolError>;
|
|
52
|
+
/**
|
|
53
|
+
* The remote ref for these bytes, under this schema.
|
|
54
|
+
*
|
|
55
|
+
* The validation hash is the delicate part, and the reason `schema` is a
|
|
56
|
+
* parameter rather than something derived here: it is baked into the ref, and
|
|
57
|
+
* the validator recomputes it from the schema it finds at the path. Hand this
|
|
58
|
+
* the wrong schema and the upload succeeds and then never validates — see
|
|
59
|
+
* `docs/plans/mcp-remote-images.md` Part D, and `remoteImageSchemaFor`, which
|
|
60
|
+
* is where a gallery's synthesized schema comes from.
|
|
61
|
+
*/
|
|
62
|
+
export declare function buildRemoteRef(input: {
|
|
63
|
+
target: RemoteUploadTarget;
|
|
64
|
+
bytes: Uint8Array;
|
|
65
|
+
/** `public/...`, with no leading slash — the shape `createRemoteRef` demands. */
|
|
66
|
+
filePath: `public/${string}`;
|
|
67
|
+
fileExt: string;
|
|
68
|
+
metadata: Record<string, unknown> | undefined;
|
|
69
|
+
schema: SerializedImageSchema | SerializedFileSchema;
|
|
70
|
+
}): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the image tool needs from an image library, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* The tool is not given `sharp`. It is given these two functions, because
|
|
5
|
+
* that is the whole of what uploading an image needs an image library for:
|
|
6
|
+
* read the dimensions out of the bytes, and re-encode them when the schema
|
|
7
|
+
* asks. Keeping the contract this narrow is what lets the host choose —
|
|
8
|
+
* `sharpImageProcessor` from `@valbuild/mcp/sharp` is the one we ship, and a
|
|
9
|
+
* project that already has an encoder can write its own in twenty lines.
|
|
10
|
+
*
|
|
11
|
+
* It also means this package has no native dependency. `sharp` ships a
|
|
12
|
+
* compiled binary per platform, and a CMS should not put one in every
|
|
13
|
+
* project's install just so that the projects which want image uploads can
|
|
14
|
+
* have them.
|
|
15
|
+
*/
|
|
16
|
+
export type ValImageProcessorResult = {
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
/** e.g. `image/webp`. */
|
|
20
|
+
mimeType: string;
|
|
21
|
+
};
|
|
22
|
+
export type ValImageEncodeRequest = {
|
|
23
|
+
/** The mime type to convert to, e.g. `image/webp`. */
|
|
24
|
+
mimeType: string;
|
|
25
|
+
/** Between 0 and 1, as `s.image({ encode: { quality } })` states it. */
|
|
26
|
+
quality: number;
|
|
27
|
+
/**
|
|
28
|
+
* The size to scale down to, or `null` when the image already fits inside
|
|
29
|
+
* the schema's bounds. Never an upscale: {@link fitWithin} answers `null`
|
|
30
|
+
* rather than a bigger box.
|
|
31
|
+
*/
|
|
32
|
+
resizeTo: {
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
} | null;
|
|
36
|
+
};
|
|
37
|
+
export type ValImageProcessor = {
|
|
38
|
+
/**
|
|
39
|
+
* Width, height and mime type of these bytes.
|
|
40
|
+
*
|
|
41
|
+
* `null` when they are not an image this processor can read — which is an
|
|
42
|
+
* answer, not a failure: the tool reports it to the caller as invalid input
|
|
43
|
+
* rather than as an internal error, because that is what it is.
|
|
44
|
+
*
|
|
45
|
+
* Must not throw. A processor that throws is treated as an internal error.
|
|
46
|
+
*/
|
|
47
|
+
read(bytes: Uint8Array): Promise<ValImageProcessorResult | null>;
|
|
48
|
+
/**
|
|
49
|
+
* Re-encode, or decline to.
|
|
50
|
+
*
|
|
51
|
+
* `null` means "I could not, upload the original" — the same answer the
|
|
52
|
+
* browser gives when a canvas is unavailable. A failed optimisation must
|
|
53
|
+
* never become a failed upload, so this is a normal outcome and not an
|
|
54
|
+
* error.
|
|
55
|
+
*/
|
|
56
|
+
encode(bytes: Uint8Array, request: ValImageEncodeRequest): Promise<{
|
|
57
|
+
bytes: Uint8Array;
|
|
58
|
+
mimeType: string;
|
|
59
|
+
} | null>;
|
|
60
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Val's content tools, and the checks that decide whether a request may reach
|
|
3
|
+
* them, for hosts that speak the Model Context Protocol.
|
|
4
|
+
*
|
|
5
|
+
* Nothing in this package imports an MCP SDK. The app owns the transport —
|
|
6
|
+
* which SDK, which route, which framework — and this owns the parts that must
|
|
7
|
+
* not be re-decided per app: the tools themselves, whether a request is allowed
|
|
8
|
+
* to reach them at all, and whose credential it carries.
|
|
9
|
+
* `docs/plans/mcp.md` Part A has the reasoning; the short version is that the
|
|
10
|
+
* SDK reorganised itself once already, and the security checks should not move
|
|
11
|
+
* when it does again.
|
|
12
|
+
*/
|
|
13
|
+
export { createValTools } from "./tools/index.js";
|
|
14
|
+
export type { ValToolsOptions } from "./tools/index.js";
|
|
15
|
+
export type { ValScope, ValToolAuth, ValToolContext, ValToolDefinition, ValToolDefinitionJson, ValToolError, ValToolErrorCode, ValToolResult, ValTools, } from "./tools/index.js";
|
|
16
|
+
export { VAL_SCOPE_READ, VAL_SCOPE_WRITE, authorIdFromVerifiedSubject, } from "./tools/index.js";
|
|
17
|
+
export { defineTool, err, ok, savePatch } from "./tools/index.js";
|
|
18
|
+
export type { OnInvalid, SavePatchData, SavePatchResult, UploadPatchFiles, ValToolDeps, ValToolImpl, ValToolState, } from "./tools/index.js";
|
|
19
|
+
export { createValImageTools } from "./images/index.js";
|
|
20
|
+
export type { ValImageProcessor, ValImageProcessorResult, ValImageEncodeRequest, } from "./images/index.js";
|
|
21
|
+
export { initValMcp } from "./initValMcp.js";
|
|
22
|
+
export type { ValMcp, ValMcpAuthorizationResult } from "./initValMcp.js";
|
|
23
|
+
export type { ValOAuthConfig } from "./valAccessToken.js";
|
|
24
|
+
export type { ValMcpMetadataHandlers } from "./valMcpMetadata.js";
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ValConfig, ValModules } from "@valbuild/core";
|
|
2
|
+
import { type ValToolContext, type ValToolImpl, type ValTools } from "./tools/index.js";
|
|
3
|
+
import { type ValOAuthConfig } from "./valAccessToken.js";
|
|
4
|
+
import { type ValMcpMetadataHandlers } from "./valMcpMetadata.js";
|
|
5
|
+
/**
|
|
6
|
+
* Val's tools over MCP, and the two checks that have to happen before a request
|
|
7
|
+
* gets to them.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here imports an MCP SDK. The app owns the transport — which SDK, which
|
|
10
|
+
* route, which framework — and this owns the parts that must not be re-decided
|
|
11
|
+
* per app: whether the request is allowed to reach the tools at all, and whose
|
|
12
|
+
* credential it carries. `docs/plans/mcp.md` Part A has the reasoning; the short
|
|
13
|
+
* version is that the SDK reorganised itself once already, and the security
|
|
14
|
+
* checks should not move when it does again.
|
|
15
|
+
*/
|
|
16
|
+
export type ValMcpAuthorizationResult = {
|
|
17
|
+
status: "ok";
|
|
18
|
+
tools: ValTools;
|
|
19
|
+
ctx: ValToolContext;
|
|
20
|
+
} | {
|
|
21
|
+
status: "refused";
|
|
22
|
+
response: Response;
|
|
23
|
+
};
|
|
24
|
+
export type ValMcp = {
|
|
25
|
+
/**
|
|
26
|
+
* Check a request and, if it is allowed, hand back the tools and the context
|
|
27
|
+
* to call them with.
|
|
28
|
+
*
|
|
29
|
+
* Call this per request — both halves are per request. Refusing early is the
|
|
30
|
+
* point: a refused request must not reach the protocol layer, let alone a
|
|
31
|
+
* tool.
|
|
32
|
+
*/
|
|
33
|
+
valMcpAuthorize: (request: Request | undefined) => Promise<ValMcpAuthorizationResult>;
|
|
34
|
+
/**
|
|
35
|
+
* The registry itself, for listing tools at startup.
|
|
36
|
+
*
|
|
37
|
+
* Listing needs no credential — it reads no content — so registering tools
|
|
38
|
+
* with an MCP server can happen once rather than per request.
|
|
39
|
+
*/
|
|
40
|
+
valMcpTools: () => Promise<ValTools>;
|
|
41
|
+
/**
|
|
42
|
+
* The RFC 9728 protected-resource document, for mounting at
|
|
43
|
+
* `/.well-known/oauth-protected-resource`.
|
|
44
|
+
*
|
|
45
|
+
* `null` when no `oauth` config was given: a project that has not been told
|
|
46
|
+
* where to authorize must not publish a document claiming it knows.
|
|
47
|
+
*/
|
|
48
|
+
valMcpMetadata: ValMcpMetadataHandlers | null;
|
|
49
|
+
};
|
|
50
|
+
export declare function initValMcp(valModules: ValModules, config: ValConfig, opts?: {
|
|
51
|
+
formatter?: (code: string, filePath: string) => string | Promise<string>;
|
|
52
|
+
/**
|
|
53
|
+
* Where to authorize, and what audience to expect.
|
|
54
|
+
*
|
|
55
|
+
* Required for proxy mode, and only omittable in local filesystem mode —
|
|
56
|
+
* where there is no authorization server to talk to and no backend to
|
|
57
|
+
* authenticate to, so local development stays a one-liner. Provide it and
|
|
58
|
+
* every call needs a verified access token, including in fs mode, where a
|
|
59
|
+
* token is refused rather than ignored: a host that thinks it is
|
|
60
|
+
* authenticating should never silently get unauthenticated local access.
|
|
61
|
+
*/
|
|
62
|
+
oauth?: ValOAuthConfig;
|
|
63
|
+
/**
|
|
64
|
+
* Tools to serve alongside the built-in ones.
|
|
65
|
+
*
|
|
66
|
+
* This is how the image tool gets in. It is not in the default set because
|
|
67
|
+
* it needs an image library to read the dimensions out of a JPEG and to
|
|
68
|
+
* re-encode one, and `sharp` is a native dependency that no project should
|
|
69
|
+
* acquire by installing a CMS. So the host constructs it —
|
|
70
|
+
* `createValImageTools(sharpImageProcessor(sharp))` — and passes it here,
|
|
71
|
+
* which is also what makes it the host's decision rather than ours.
|
|
72
|
+
*
|
|
73
|
+
* A name that collides with a built-in tool is refused rather than
|
|
74
|
+
* silently winning or silently losing: an agent that called `get_source`
|
|
75
|
+
* and reached something else would be a genuinely confusing afternoon.
|
|
76
|
+
*/
|
|
77
|
+
extraTools?: ValToolImpl[];
|
|
78
|
+
/**
|
|
79
|
+
* The Val API route the Studio is mounted on. Only the data layer reads it.
|
|
80
|
+
*/
|
|
81
|
+
route?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Package versions, for a host that has its own.
|
|
84
|
+
*
|
|
85
|
+
* `initHandlerOptions` refuses to build a proxy-mode config without both,
|
|
86
|
+
* so a Next app passes its own version through
|
|
87
|
+
* (`@valbuild/next/server`'s `initValMcp` does this for you). A host that
|
|
88
|
+
* is not Next gets this package's version, which is the honest answer: it
|
|
89
|
+
* is the package serving the request.
|
|
90
|
+
*/
|
|
91
|
+
versions?: {
|
|
92
|
+
core?: string;
|
|
93
|
+
next?: string;
|
|
94
|
+
};
|
|
95
|
+
}): ValMcp;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ValImageProcessor } from "../images/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* `sharp`, as the image tool wants it.
|
|
4
|
+
*
|
|
5
|
+
* The library is passed IN rather than imported, and that is the whole point of
|
|
6
|
+
* this file existing separately. `sharp` ships a compiled binary per platform;
|
|
7
|
+
* importing it here would put one in every project that installs Val, for a
|
|
8
|
+
* feature most of them will not mount. Instead the project installs `sharp`
|
|
9
|
+
* itself and writes:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import sharp from "sharp";
|
|
13
|
+
* import { createValImageTools } from "@valbuild/mcp";
|
|
14
|
+
* import { sharpImageProcessor } from "@valbuild/mcp/sharp";
|
|
15
|
+
*
|
|
16
|
+
* const tools = createValImageTools(sharpImageProcessor(sharp));
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* The parameter is typed structurally — {@link SharpLike} — rather than as
|
|
20
|
+
* `import("sharp")`, for the same reason: a type-only import of a package that
|
|
21
|
+
* is not installed is still a compile error, and this package must typecheck in
|
|
22
|
+
* a project that has never heard of sharp. The real `sharp` satisfies it, and
|
|
23
|
+
* `sharpImageProcessor.test.ts` asserts as much against the actual library so
|
|
24
|
+
* the structural type cannot drift away from it unnoticed.
|
|
25
|
+
*/
|
|
26
|
+
/** What `sharp(bytes)` gives back, narrowed to what this file calls. */
|
|
27
|
+
export type SharpPipelineLike = {
|
|
28
|
+
metadata(): Promise<{
|
|
29
|
+
width?: number | undefined;
|
|
30
|
+
height?: number | undefined;
|
|
31
|
+
format?: string | undefined;
|
|
32
|
+
}>;
|
|
33
|
+
resize(options: {
|
|
34
|
+
width?: number | undefined;
|
|
35
|
+
height?: number | undefined;
|
|
36
|
+
fit?: "inside" | undefined;
|
|
37
|
+
withoutEnlargement?: boolean | undefined;
|
|
38
|
+
}): SharpPipelineLike;
|
|
39
|
+
rotate(): SharpPipelineLike;
|
|
40
|
+
webp(options: {
|
|
41
|
+
quality?: number | undefined;
|
|
42
|
+
}): SharpPipelineLike;
|
|
43
|
+
toBuffer(): Promise<Buffer>;
|
|
44
|
+
};
|
|
45
|
+
export type SharpLike = (input: Uint8Array) => SharpPipelineLike;
|
|
46
|
+
export declare function sharpImageProcessor(sharp: SharpLike): ValImageProcessor;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ValModules } from "@valbuild/core";
|
|
2
|
+
import { type ValOps, type ValServerConfig } from "@valbuild/server";
|
|
3
|
+
import type { ValToolImpl, ValToolState } from "./defineTool.js";
|
|
4
|
+
import { type ValToolError, type ValTools } from "./types.js";
|
|
5
|
+
export type ValToolsOptions = ValServerConfig;
|
|
6
|
+
/**
|
|
7
|
+
* Val's server-side tool registry.
|
|
8
|
+
*
|
|
9
|
+
* This is the piece Val did not have: the Studio's chat tools are defined *and
|
|
10
|
+
* executed in the browser*, against its client stores, so nothing here could be
|
|
11
|
+
* re-exposed. These tools run against {@link ValOps} instead, which is what lets
|
|
12
|
+
* an MCP server — or a stdio transport, or anything else — drive Val content
|
|
13
|
+
* without a browser.
|
|
14
|
+
*
|
|
15
|
+
* Transport-agnostic on purpose: nothing under `tools/` imports an MCP SDK. A
|
|
16
|
+
* host adapts {@link ValToolResult} at its own edge.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createValTools(valModules: ValModules, options: ValToolsOptions,
|
|
19
|
+
/**
|
|
20
|
+
* Tools the host built itself, served alongside the built-in ones.
|
|
21
|
+
*
|
|
22
|
+
* The image tool arrives this way: it needs an image library the host has to
|
|
23
|
+
* install, so it cannot be constructed here. See `createValImageTools`.
|
|
24
|
+
*/
|
|
25
|
+
extraTools?: ValToolImpl[]): ValTools;
|
|
26
|
+
/**
|
|
27
|
+
* The content as the caller should see it, loaded once per call.
|
|
28
|
+
*
|
|
29
|
+
* Pending patches are applied, because an agent looking at a project mid-edit
|
|
30
|
+
* should see what the Studio would show rather than the last published state.
|
|
31
|
+
*
|
|
32
|
+
* Deliberately not cached across calls. In fs mode a save recomputes the base
|
|
33
|
+
* sha within the same process, so a cached view would go stale silently — and
|
|
34
|
+
* the cost of being wrong here is an agent writing a patch against content that
|
|
35
|
+
* has already moved.
|
|
36
|
+
*
|
|
37
|
+
* Exported for `toolsFixture`, not for consumers: `tools/index.ts` re-exports by
|
|
38
|
+
* name, so this stays inside the package. A test that assembled its own state
|
|
39
|
+
* would be asserting against a view no real call ever sees.
|
|
40
|
+
*/
|
|
41
|
+
export declare function loadState(ops: ValOps): Promise<{
|
|
42
|
+
status: "ok";
|
|
43
|
+
state: ValToolState;
|
|
44
|
+
} | {
|
|
45
|
+
status: "error";
|
|
46
|
+
result: ValToolError;
|
|
47
|
+
}>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Json, ModuleFilePath, PatchId, SerializedSchema } from "@valbuild/core";
|
|
2
|
+
import type { z } from "zod";
|
|
3
|
+
import type { ValOps, OrderedPatches, PatchAnalysis, Schemas, Sources, ValServerConfig } from "@valbuild/server";
|
|
4
|
+
import type { ValToolContext, ValToolDefinition, ValToolError, ValToolErrorCode, ValToolResult } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* How a tool is written, and what it is handed.
|
|
7
|
+
*
|
|
8
|
+
* Tools are defined with {@link defineTool} so that the handler's `args` are
|
|
9
|
+
* inferred from the tool's own `inputSchema`. Without that the array of tools
|
|
10
|
+
* would have to be typed at its widest and every handler would start by
|
|
11
|
+
* re-narrowing `unknown`, which is exactly where a tool and its schema drift
|
|
12
|
+
* apart unnoticed.
|
|
13
|
+
*/
|
|
14
|
+
/** Everything a tool is allowed to reach. Deliberately narrow. */
|
|
15
|
+
export type ValToolDeps = {
|
|
16
|
+
/**
|
|
17
|
+
* The data layer for *this call*. In proxy mode it is authenticated as the
|
|
18
|
+
* caller, so a tool must never reach for an ambient instance instead.
|
|
19
|
+
*/
|
|
20
|
+
ops: ValOps;
|
|
21
|
+
/**
|
|
22
|
+
* How this project is configured — the same object `createValOps` was built
|
|
23
|
+
* from.
|
|
24
|
+
*
|
|
25
|
+
* Here because remote images need it: which credential talks to the content
|
|
26
|
+
* host about them is a property of the project, not of the caller, and a tool
|
|
27
|
+
* that had to be handed it separately would be handed a second copy of an
|
|
28
|
+
* answer the registry already has.
|
|
29
|
+
*/
|
|
30
|
+
config: ValServerConfig;
|
|
31
|
+
ctx: ValToolContext;
|
|
32
|
+
/**
|
|
33
|
+
* The content as the caller should see it: base sources with pending patches
|
|
34
|
+
* applied, plus the schemas and the patch analysis that produced them.
|
|
35
|
+
*
|
|
36
|
+
* Loaded once per call and shared, because a tool that re-derived it would
|
|
37
|
+
* both pay for it again and risk reading a different revision than the one it
|
|
38
|
+
* validated against.
|
|
39
|
+
*/
|
|
40
|
+
state: ValToolState;
|
|
41
|
+
};
|
|
42
|
+
export type ValToolState = {
|
|
43
|
+
schemas: Schemas;
|
|
44
|
+
serializedSchemas: Record<ModuleFilePath, SerializedSchema>;
|
|
45
|
+
sources: Sources;
|
|
46
|
+
patches: OrderedPatches;
|
|
47
|
+
analysis: PatchAnalysis;
|
|
48
|
+
/**
|
|
49
|
+
* Pending patches that would not apply, by the module they belong to.
|
|
50
|
+
*
|
|
51
|
+
* A module listed here has `sources` that silently lack those changes, so its
|
|
52
|
+
* content is not what publishing would produce. Reads still return it — it is
|
|
53
|
+
* what the Studio would show too, and a project has to stay diagnosable — but
|
|
54
|
+
* a write to such a module is refused, because it would be based on a state
|
|
55
|
+
* that does not exist.
|
|
56
|
+
*/
|
|
57
|
+
unappliedPatches: Record<ModuleFilePath, {
|
|
58
|
+
patchId: PatchId;
|
|
59
|
+
skipped: boolean;
|
|
60
|
+
error: {
|
|
61
|
+
message: string;
|
|
62
|
+
};
|
|
63
|
+
}[]>;
|
|
64
|
+
};
|
|
65
|
+
export type ValToolImpl = ValToolDefinition & {
|
|
66
|
+
handler: (args: unknown, deps: ValToolDeps) => Promise<ValToolResult>;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Declare a tool, binding its handler to its input schema.
|
|
70
|
+
*
|
|
71
|
+
* The handler receives already-parsed arguments: the registry validates against
|
|
72
|
+
* `inputSchema` before calling, so a handler never sees input its schema would
|
|
73
|
+
* have rejected.
|
|
74
|
+
*/
|
|
75
|
+
export declare function defineTool<S extends z.ZodType>(definition: Omit<ValToolDefinition, "inputSchema"> & {
|
|
76
|
+
inputSchema: S;
|
|
77
|
+
}, handler: (args: z.infer<S>, deps: ValToolDeps) => Promise<ValToolResult>): ValToolImpl;
|
|
78
|
+
export declare function ok(data: Json): ValToolResult;
|
|
79
|
+
export declare function err(code: ValToolErrorCode, message: string): ValToolError;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { createValTools, type ValToolsOptions } from "./createValTools.js";
|
|
2
|
+
export { defineTool, err, ok } from "./defineTool.js";
|
|
3
|
+
export type { ValToolDeps, ValToolImpl, ValToolState } from "./defineTool.js";
|
|
4
|
+
export { savePatch, mintPatchId, deriveParentRef } from "./writePath.js";
|
|
5
|
+
export type { OnInvalid, SavePatchData, SavePatchResult, UploadPatchFiles, } from "./writePath.js";
|
|
6
|
+
export { VAL_SCOPE_READ, VAL_SCOPE_WRITE, authorIdFromVerifiedSubject, } from "./types.js";
|
|
7
|
+
export type { ValScope, ValToolAuth, ValToolContext, ValToolDefinition, ValToolDefinitionJson, ValToolError, ValToolErrorCode, ValToolResult, ValTools, } from "./types.js";
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { AuthorId } from "@valbuild/server";
|
|
2
|
+
import type { Json } from "@valbuild/core";
|
|
3
|
+
import type { z } from "zod";
|
|
4
|
+
/**
|
|
5
|
+
* The public surface of Val's server-side tool registry.
|
|
6
|
+
*
|
|
7
|
+
* Types only, deliberately: this file is the contract that the MCP hosts, the
|
|
8
|
+
* CLI's stdio transport and the tools themselves are all written against, and
|
|
9
|
+
* keeping it free of implementation means those can be built in any order
|
|
10
|
+
* without one of them owning the shape.
|
|
11
|
+
*
|
|
12
|
+
* The design this implements is `docs/plans/mcp.md`, Part A. Two constraints
|
|
13
|
+
* from it are load-bearing and easy to break by accident:
|
|
14
|
+
*
|
|
15
|
+
* 1. **Nothing here may import an MCP SDK.** That is what lets hosts other
|
|
16
|
+
* than the template consume these tools, and it is not hypothetical
|
|
17
|
+
* hygiene — the TypeScript SDK reorganised itself at v2.0.0, and a registry
|
|
18
|
+
* coupled to it would have moved with it.
|
|
19
|
+
* 2. **The result type is deliberately not MCP's `CallToolResult`.** Each host
|
|
20
|
+
* adapts {@link ValToolResult} at its own edge, which is also where an
|
|
21
|
+
* error becomes an in-band `isError` result the model can recover from
|
|
22
|
+
* rather than a transport failure.
|
|
23
|
+
*/
|
|
24
|
+
/** Why a tool call failed, in a form a host can map onto its own errors. */
|
|
25
|
+
export type ValToolErrorCode =
|
|
26
|
+
/** No tool of that name is registered. */
|
|
27
|
+
"unknown-tool"
|
|
28
|
+
/** The arguments did not satisfy the tool's `inputSchema`. */
|
|
29
|
+
| "invalid-args"
|
|
30
|
+
/** The path, module or key the arguments name does not exist. */
|
|
31
|
+
| "not-found"
|
|
32
|
+
/** The caller is not allowed to do this — wrong credential, or none. */
|
|
33
|
+
| "forbidden"
|
|
34
|
+
/**
|
|
35
|
+
* The write was rejected because applying it would leave content invalid.
|
|
36
|
+
* Nothing was stored. See `docs/plans/mcp.md` Part C on speculative
|
|
37
|
+
* validation.
|
|
38
|
+
*/
|
|
39
|
+
| "validation-failed"
|
|
40
|
+
/**
|
|
41
|
+
* Another writer moved the patch chain first. Callers may re-derive the
|
|
42
|
+
* parent ref and retry once; the registry does that itself before
|
|
43
|
+
* surfacing this.
|
|
44
|
+
*/
|
|
45
|
+
| "conflict"
|
|
46
|
+
/** The tool exists but is not available in this mode (e.g. fs vs http). */
|
|
47
|
+
| "unsupported"
|
|
48
|
+
/** Anything else, including an upstream failure. */
|
|
49
|
+
| "internal";
|
|
50
|
+
export type ValToolDefinition = {
|
|
51
|
+
name: string;
|
|
52
|
+
title?: string;
|
|
53
|
+
description: string;
|
|
54
|
+
/** zod v4 — a Standard Schema, which is what the MCP SDKs consume. */
|
|
55
|
+
inputSchema: z.ZodType;
|
|
56
|
+
/**
|
|
57
|
+
* MCP tool annotations. Hints, not enforcement: a host may show them to a
|
|
58
|
+
* user or use them to decide what to auto-approve, so they have to be
|
|
59
|
+
* honest about what the tool does.
|
|
60
|
+
*/
|
|
61
|
+
annotations?: {
|
|
62
|
+
readOnlyHint?: boolean;
|
|
63
|
+
destructiveHint?: boolean;
|
|
64
|
+
idempotentHint?: boolean;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The same definition with `inputSchema` as JSON Schema, for hosts that want the
|
|
69
|
+
* wire shape rather than a Standard Schema.
|
|
70
|
+
*
|
|
71
|
+
* Typed as whatever zod's own converter produces, so deriving it needs no cast
|
|
72
|
+
* and no second hand-written description of the same input.
|
|
73
|
+
*/
|
|
74
|
+
export type ValToolDefinitionJson = Omit<ValToolDefinition, "inputSchema"> & {
|
|
75
|
+
inputSchema: ReturnType<typeof z.toJSONSchema<z.ZodType>>;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* How the caller was established, and there is one acceptable answer: the host
|
|
79
|
+
* **checked a signature**.
|
|
80
|
+
*
|
|
81
|
+
* A union of one, deliberately. It carried a second variant — a personal access
|
|
82
|
+
* token relayed to the backend unchecked, on the reasoning that the app cannot
|
|
83
|
+
* resolve one and the backend can. The reasoning held; the shape did not. A
|
|
84
|
+
* credential the host cannot check is one it also cannot refuse, so accepting
|
|
85
|
+
* one made "a deployed endpoint that authenticates nobody" a supported
|
|
86
|
+
* configuration, and it let a host serve these tools without ever being told
|
|
87
|
+
* where callers should authorize. The discriminant stays so that adding a
|
|
88
|
+
* second *verified* kind stays a one-line change at every call site.
|
|
89
|
+
*/
|
|
90
|
+
export type ValToolAuth = {
|
|
91
|
+
type: "verified-profile";
|
|
92
|
+
/**
|
|
93
|
+
* The profile the host **verified** — the `sub` of an access token whose
|
|
94
|
+
* signature, issuer, audience and expiry were all checked against the
|
|
95
|
+
* authorization server's published key.
|
|
96
|
+
*
|
|
97
|
+
* This field is why an identity field exists at all, and an earlier version
|
|
98
|
+
* of this file argued none should. That argument was about a specific case
|
|
99
|
+
* and stated too broadly: an id the host *asserts* on the strength of a
|
|
100
|
+
* credential it cannot check is an unverified claim dressed as a checked one,
|
|
101
|
+
* and that is still refused — it is why a relayed token never carried a
|
|
102
|
+
* profile, and now cannot reach here at all. An id the host *verified*
|
|
103
|
+
* cryptographically is a different thing, and it is the same standing the
|
|
104
|
+
* Studio has when it re-signs a session it established itself.
|
|
105
|
+
*/
|
|
106
|
+
profileId: AuthorId;
|
|
107
|
+
/**
|
|
108
|
+
* The token's granted scopes, as the authorization server issued them.
|
|
109
|
+
*
|
|
110
|
+
* Enforced here as well as by the backend, deliberately. Two checks on one
|
|
111
|
+
* grant is not redundancy for its own sake: this one can refuse a write
|
|
112
|
+
* before it is attempted, so a token that may only read never reaches the
|
|
113
|
+
* code that builds a patch.
|
|
114
|
+
*/
|
|
115
|
+
scopes: string[];
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Who is calling, established once per request by the host.
|
|
119
|
+
*
|
|
120
|
+
* `null` means local fs mode, where there is no credential to hold and patches
|
|
121
|
+
* are written with no author, exactly as the Studio does locally (D.1). In
|
|
122
|
+
* proxy mode `null` is refused rather than falling back to the app's own API
|
|
123
|
+
* key: that key can do more than any single user, and quietly substituting it
|
|
124
|
+
* would turn a missing credential into full access.
|
|
125
|
+
*/
|
|
126
|
+
export type ValToolContext = {
|
|
127
|
+
auth: ValToolAuth | null;
|
|
128
|
+
/** Groups a run of related edits, when the host has such a notion. */
|
|
129
|
+
sessionId: string | null;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Brand a verified subject as an {@link AuthorId}.
|
|
133
|
+
*
|
|
134
|
+
* `AuthorId` is a branded string so that an id cannot be conjured from any
|
|
135
|
+
* string that happens to be lying around — which is exactly the mistake this
|
|
136
|
+
* type is guarding against. That makes one assertion unavoidable at the boundary
|
|
137
|
+
* where a real id enters the system, so it lives here, once, with a name that
|
|
138
|
+
* says what makes it legitimate: the caller has *verified* this subject, not
|
|
139
|
+
* received it.
|
|
140
|
+
*
|
|
141
|
+
* Do not reach for this to satisfy a type. If you are holding a string you did
|
|
142
|
+
* not verify, the honest value is `null`.
|
|
143
|
+
*/
|
|
144
|
+
export declare function authorIdFromVerifiedSubject(subject: string): AuthorId;
|
|
145
|
+
/** Read access. Every call needs it, the writes included. */
|
|
146
|
+
export declare const VAL_SCOPE_READ = "val:read";
|
|
147
|
+
/** Write access. Needed *in addition* by any tool not marked `readOnlyHint`. */
|
|
148
|
+
export declare const VAL_SCOPE_WRITE = "val:write";
|
|
149
|
+
export type ValScope = typeof VAL_SCOPE_READ | typeof VAL_SCOPE_WRITE;
|
|
150
|
+
export type ValToolResult = {
|
|
151
|
+
status: "ok";
|
|
152
|
+
data: Json;
|
|
153
|
+
} | ValToolError;
|
|
154
|
+
/**
|
|
155
|
+
* The failure half of {@link ValToolResult}, on its own.
|
|
156
|
+
*
|
|
157
|
+
* Named because several internal steps can only fail — speculative validation
|
|
158
|
+
* rejecting a patch, an upload that did not land — and typing those as the
|
|
159
|
+
* whole union means the compiler cannot see that an early return is in fact an
|
|
160
|
+
* error return.
|
|
161
|
+
*/
|
|
162
|
+
export type ValToolError = {
|
|
163
|
+
status: "error";
|
|
164
|
+
code: ValToolErrorCode;
|
|
165
|
+
message: string;
|
|
166
|
+
};
|
|
167
|
+
export type ValTools = {
|
|
168
|
+
list(): ValToolDefinition[];
|
|
169
|
+
listJsonSchema(): ValToolDefinitionJson[];
|
|
170
|
+
call(name: string, args: unknown, ctx: ValToolContext): Promise<ValToolResult>;
|
|
171
|
+
dispose(): Promise<void>;
|
|
172
|
+
};
|