@setluca/mcp 0.1.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 +156 -0
- package/dist/config.js +47 -0
- package/dist/errors.js +25 -0
- package/dist/generated/openapi-inputs.js +2926 -0
- package/dist/generated/openapi-outputs.js +13798 -0
- package/dist/http.js +148 -0
- package/dist/index.js +12 -0
- package/dist/oauth-resolver.js +55 -0
- package/dist/observability.js +19 -0
- package/dist/openapi-zod.js +269 -0
- package/dist/operations.js +780 -0
- package/dist/prompts.js +79 -0
- package/dist/provenance.js +15 -0
- package/dist/remote.js +76 -0
- package/dist/resources.js +38 -0
- package/dist/server.js +375 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +74 -0
package/dist/http.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import { LucaConfig } from "./config.js";
|
|
3
|
+
import { LucaDecodeError, LucaHttpError, LucaNetworkError, } from "./errors.js";
|
|
4
|
+
export const LucaApi = Context.GenericTag("@luca/mcp/LucaApi");
|
|
5
|
+
function isPresent(value) {
|
|
6
|
+
return value !== undefined && value !== null && value !== "";
|
|
7
|
+
}
|
|
8
|
+
function encodePath(path, params) {
|
|
9
|
+
return path.replace(/\{([^}]+)\}/g, (_, key) => {
|
|
10
|
+
const value = params?.[key];
|
|
11
|
+
if (!value) {
|
|
12
|
+
throw new Error(`Missing path parameter: ${key}`);
|
|
13
|
+
}
|
|
14
|
+
return encodeURIComponent(value);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function appendQuery(url, query) {
|
|
18
|
+
if (!query) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
for (const [key, value] of Object.entries(query)) {
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
for (const item of value) {
|
|
24
|
+
if (isPresent(item)) {
|
|
25
|
+
url.searchParams.append(key, String(item));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else if (isPresent(value)) {
|
|
30
|
+
url.searchParams.set(key, String(value));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function parseJson(text) {
|
|
35
|
+
if (!text) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(text);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new LucaDecodeError({
|
|
43
|
+
message: "Luca API returned non-JSON content",
|
|
44
|
+
body: text,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function buildUrl(config, input) {
|
|
49
|
+
const path = encodePath(input.operation.path, input.pathParams);
|
|
50
|
+
const url = new URL(path, `${config.apiBaseUrl}/`);
|
|
51
|
+
appendQuery(url, input.query);
|
|
52
|
+
return url;
|
|
53
|
+
}
|
|
54
|
+
function buildHeaders(config, input) {
|
|
55
|
+
const headers = new Headers({
|
|
56
|
+
accept: "application/json",
|
|
57
|
+
"user-agent": "luca-mcp/0.1.0",
|
|
58
|
+
});
|
|
59
|
+
setAuthHeaders(headers, config);
|
|
60
|
+
setWorkspaceHeaders(headers, config, input);
|
|
61
|
+
setPayloadHeaders(headers, input);
|
|
62
|
+
return headers;
|
|
63
|
+
}
|
|
64
|
+
function setAuthHeaders(headers, config) {
|
|
65
|
+
if (config.authHeader === "authorization") {
|
|
66
|
+
headers.set("authorization", `Bearer ${config.apiKey}`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
headers.set("x-api-key", config.apiKey);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function setWorkspaceHeaders(headers, config, input) {
|
|
73
|
+
const workspaceId = input.workspace?.workspaceId ?? config.workspaceId;
|
|
74
|
+
const workspaceSlug = input.workspace?.workspaceSlug ?? config.workspaceSlug;
|
|
75
|
+
if (workspaceId) {
|
|
76
|
+
headers.set("x-luca-workspace-id", workspaceId);
|
|
77
|
+
}
|
|
78
|
+
if (workspaceSlug) {
|
|
79
|
+
headers.set("x-luca-workspace-slug", workspaceSlug);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function setPayloadHeaders(headers, input) {
|
|
83
|
+
const hasBody = input.operation.method !== "GET" && input.body !== undefined;
|
|
84
|
+
if (hasBody) {
|
|
85
|
+
headers.set("content-type", "application/json");
|
|
86
|
+
}
|
|
87
|
+
if (input.operation.idempotencyRequired) {
|
|
88
|
+
headers.set("idempotency-key", input.idempotencyKey ?? crypto.randomUUID());
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function buildRequestInit(input, headers) {
|
|
92
|
+
const hasBody = input.operation.method !== "GET" && input.body !== undefined;
|
|
93
|
+
return {
|
|
94
|
+
method: input.operation.method,
|
|
95
|
+
headers,
|
|
96
|
+
...(hasBody ? { body: JSON.stringify(input.body) } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function readResponseBody(response) {
|
|
100
|
+
return Effect.gen(function* () {
|
|
101
|
+
const text = yield* Effect.tryPromise({
|
|
102
|
+
try: () => response.text(),
|
|
103
|
+
catch: (cause) => new LucaNetworkError({
|
|
104
|
+
message: "Could not read Luca API response body",
|
|
105
|
+
cause,
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
return yield* Effect.try({
|
|
109
|
+
try: () => parseJson(text),
|
|
110
|
+
catch: (cause) => cause instanceof LucaDecodeError
|
|
111
|
+
? cause
|
|
112
|
+
: new LucaDecodeError({ message: String(cause), body: text }),
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
export function makeLucaApi(config) {
|
|
117
|
+
return {
|
|
118
|
+
request: (input) => Effect.gen(function* () {
|
|
119
|
+
const url = buildUrl(config, input);
|
|
120
|
+
const init = buildRequestInit(input, buildHeaders(config, input));
|
|
121
|
+
const response = yield* Effect.tryPromise({
|
|
122
|
+
try: (signal) => fetch(url, {
|
|
123
|
+
...init,
|
|
124
|
+
signal,
|
|
125
|
+
}),
|
|
126
|
+
catch: (cause) => new LucaNetworkError({
|
|
127
|
+
message: `Could not reach Luca API at ${url.toString()}`,
|
|
128
|
+
cause,
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
const json = yield* readResponseBody(response);
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
const requestId = response.headers.get("x-request-id") ?? undefined;
|
|
134
|
+
return yield* Effect.fail(new LucaHttpError({
|
|
135
|
+
status: response.status,
|
|
136
|
+
statusText: response.statusText,
|
|
137
|
+
body: json,
|
|
138
|
+
...(requestId ? { requestId } : {}),
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
return json;
|
|
142
|
+
}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
export const LucaApiLive = Layer.effect(LucaApi, Effect.gen(function* () {
|
|
146
|
+
const config = yield* LucaConfig;
|
|
147
|
+
return makeLucaApi(config);
|
|
148
|
+
}));
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { createLucaServer } from "./server.js";
|
|
4
|
+
async function main() {
|
|
5
|
+
const server = createLucaServer();
|
|
6
|
+
const transport = new StdioServerTransport();
|
|
7
|
+
await server.connect(transport);
|
|
8
|
+
}
|
|
9
|
+
main().catch((error) => {
|
|
10
|
+
console.error("Fatal error in Luca MCP server:", error);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const TRAILING_SLASHES = /\/+$/;
|
|
2
|
+
/**
|
|
3
|
+
* Developer API keys are handled by apiKeyResolver and never leave this
|
|
4
|
+
* process via the resolve endpoint. Refresh tokens (luca_ort_) are not
|
|
5
|
+
* developer keys — they fall through to the resolver, which rejects them
|
|
6
|
+
* server-side (fail closed, no client-side oracle).
|
|
7
|
+
*/
|
|
8
|
+
const API_KEY_SHAPE = /^luca_(?!ort_)[a-f0-9]/i;
|
|
9
|
+
/**
|
|
10
|
+
* Resolver for OAuth access tokens (docs/specs/mcp-oauth-authorization-server.md
|
|
11
|
+
* §5.2): one authenticated call to the Luca API's POST /oauth/resolve, which
|
|
12
|
+
* verifies the JWT and returns the hidden vended key for the token's own
|
|
13
|
+
* grant. Fail closed: any error, non-200, or malformed body → null → 401.
|
|
14
|
+
*/
|
|
15
|
+
export function oauthResolver(env, fetchImpl = fetch) {
|
|
16
|
+
const apiBaseUrl = (env.LUCA_API_BASE_URL ?? "https://api.setluca.com").replace(TRAILING_SLASHES, "");
|
|
17
|
+
const authHeader = env.LUCA_AUTH_HEADER ?? "x-api-key";
|
|
18
|
+
return async (token) => {
|
|
19
|
+
if (API_KEY_SHAPE.test(token)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const response = await fetchImpl(`${apiBaseUrl}/oauth/resolve`, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { authorization: `Bearer ${token}` },
|
|
26
|
+
});
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const payload = (await response.json());
|
|
31
|
+
if (!payload.apiKey) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
config: { apiBaseUrl, apiKey: payload.apiKey, authHeader },
|
|
36
|
+
scopes: payload.scopes ?? [],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Try resolvers in order; the first non-null identity wins. */
|
|
45
|
+
export function chainResolvers(...resolvers) {
|
|
46
|
+
return async (token) => {
|
|
47
|
+
for (const resolve of resolvers) {
|
|
48
|
+
const identity = await resolve(token);
|
|
49
|
+
if (identity) {
|
|
50
|
+
return identity;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One structured line per agent-surface event. PII-safe by contract: pass only
|
|
3
|
+
* names, booleans, counts, and already-coach-scoped ids — never message text,
|
|
4
|
+
* guidance text, or reasoning strings.
|
|
5
|
+
*
|
|
6
|
+
* A local mirror of `@luca/observability`'s `logAgentSurfaceEvent`: this package
|
|
7
|
+
* builds with plain `tsc` and ships to npm as a self-contained stdio tarball, so
|
|
8
|
+
* it cannot import the private workspace package. The two definitions share one
|
|
9
|
+
* contract (event names + payload shape).
|
|
10
|
+
*/
|
|
11
|
+
export function logAgentSurfaceEvent(event, fields) {
|
|
12
|
+
const clean = { msg: "mcp.agent_surface", event };
|
|
13
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
14
|
+
if (v !== undefined) {
|
|
15
|
+
clean[k] = v;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
console.log(JSON.stringify(clean));
|
|
19
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
function isNullable(schema) {
|
|
3
|
+
return (schema.nullable === true ||
|
|
4
|
+
(Array.isArray(schema.type) && schema.type.includes("null")));
|
|
5
|
+
}
|
|
6
|
+
function nonNullTypes(schema) {
|
|
7
|
+
if (Array.isArray(schema.type)) {
|
|
8
|
+
return schema.type.filter((type) => type !== "null");
|
|
9
|
+
}
|
|
10
|
+
if (schema.type) {
|
|
11
|
+
return [schema.type];
|
|
12
|
+
}
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
function withDescription(schema, description) {
|
|
16
|
+
return description ? schema.describe(description) : schema;
|
|
17
|
+
}
|
|
18
|
+
function enumSchema(values) {
|
|
19
|
+
if (values.length === 0) {
|
|
20
|
+
return z.never();
|
|
21
|
+
}
|
|
22
|
+
if (values.every((value) => typeof value === "string")) {
|
|
23
|
+
return z.enum(values);
|
|
24
|
+
}
|
|
25
|
+
if (values.length === 1) {
|
|
26
|
+
return z.literal(values[0]);
|
|
27
|
+
}
|
|
28
|
+
const literals = values.map((value) => z.literal(value));
|
|
29
|
+
return z.union(literals);
|
|
30
|
+
}
|
|
31
|
+
function stringSchema(schema) {
|
|
32
|
+
let result = z.string();
|
|
33
|
+
if (schema.format === "uuid") {
|
|
34
|
+
result = result.uuid();
|
|
35
|
+
}
|
|
36
|
+
else if (schema.format === "uri") {
|
|
37
|
+
result = result.url();
|
|
38
|
+
}
|
|
39
|
+
else if (schema.format === "date-time") {
|
|
40
|
+
result = result.datetime({ offset: true });
|
|
41
|
+
}
|
|
42
|
+
if (schema.minLength !== undefined) {
|
|
43
|
+
result = result.min(schema.minLength);
|
|
44
|
+
}
|
|
45
|
+
if (schema.maxLength !== undefined) {
|
|
46
|
+
result = result.max(schema.maxLength);
|
|
47
|
+
}
|
|
48
|
+
if (schema.pattern) {
|
|
49
|
+
result = result.regex(new RegExp(schema.pattern));
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
function numberSchema(schema, integer) {
|
|
54
|
+
let result = integer ? z.number().int() : z.number();
|
|
55
|
+
if (schema.minimum !== undefined) {
|
|
56
|
+
result =
|
|
57
|
+
schema.exclusiveMinimum === true
|
|
58
|
+
? result.gt(schema.minimum)
|
|
59
|
+
: result.min(schema.minimum);
|
|
60
|
+
}
|
|
61
|
+
if (schema.maximum !== undefined) {
|
|
62
|
+
result =
|
|
63
|
+
schema.exclusiveMaximum === true
|
|
64
|
+
? result.lt(schema.maximum)
|
|
65
|
+
: result.max(schema.maximum);
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
function arraySchema(schema) {
|
|
70
|
+
let result = z.array(schema.items ? schemaToZod(schema.items) : z.unknown());
|
|
71
|
+
if (schema.minItems !== undefined) {
|
|
72
|
+
result = result.min(schema.minItems);
|
|
73
|
+
}
|
|
74
|
+
if (schema.maxItems !== undefined) {
|
|
75
|
+
result = result.max(schema.maxItems);
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
function scalarSchema(schema, type) {
|
|
80
|
+
if (type === "string") {
|
|
81
|
+
return stringSchema(schema);
|
|
82
|
+
}
|
|
83
|
+
if (type === "integer" || type === "number") {
|
|
84
|
+
return numberSchema(schema, type === "integer");
|
|
85
|
+
}
|
|
86
|
+
return scalarSchemaWithoutConstraints(schema, type);
|
|
87
|
+
}
|
|
88
|
+
function scalarSchemaWithoutConstraints(schema, type) {
|
|
89
|
+
switch (type) {
|
|
90
|
+
case "boolean":
|
|
91
|
+
return z.boolean();
|
|
92
|
+
case "array":
|
|
93
|
+
return arraySchema(schema);
|
|
94
|
+
case "object":
|
|
95
|
+
return objectSchema(schema);
|
|
96
|
+
default:
|
|
97
|
+
return z.unknown();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function objectSchema(schema) {
|
|
101
|
+
const properties = schema.properties ?? {};
|
|
102
|
+
const required = new Set(schema.required ?? []);
|
|
103
|
+
const shape = {};
|
|
104
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
105
|
+
const propertySchema = schemaToZod(property);
|
|
106
|
+
shape[key] = required.has(key) ? propertySchema : propertySchema.optional();
|
|
107
|
+
}
|
|
108
|
+
const result = z.object(shape);
|
|
109
|
+
if (schema.additionalProperties === true) {
|
|
110
|
+
return result.catchall(z.unknown());
|
|
111
|
+
}
|
|
112
|
+
if (schema.additionalProperties &&
|
|
113
|
+
typeof schema.additionalProperties === "object") {
|
|
114
|
+
return result.catchall(schemaToZod(schema.additionalProperties));
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
function schemaToZod(schema) {
|
|
119
|
+
const literal = literalOrEnumSchema(schema);
|
|
120
|
+
if (literal) {
|
|
121
|
+
return literal;
|
|
122
|
+
}
|
|
123
|
+
const composite = compositeSchema(schema);
|
|
124
|
+
if (composite) {
|
|
125
|
+
return composite;
|
|
126
|
+
}
|
|
127
|
+
return describedBaseSchema(schema);
|
|
128
|
+
}
|
|
129
|
+
function literalOrEnumSchema(schema) {
|
|
130
|
+
if (schema.const !== undefined) {
|
|
131
|
+
return z.literal(schema.const);
|
|
132
|
+
}
|
|
133
|
+
if (schema.enum) {
|
|
134
|
+
return withDescription(enumSchema(schema.enum), schema.description);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
function compositeSchema(schema) {
|
|
139
|
+
const variants = schema.anyOf ?? schema.oneOf;
|
|
140
|
+
if (variants?.length) {
|
|
141
|
+
return variantSchema(schema, variants);
|
|
142
|
+
}
|
|
143
|
+
const allOf = schema.allOf;
|
|
144
|
+
if (allOf?.length) {
|
|
145
|
+
return withDescription(schemaToZod(mergeAllOf(allOf)), schema.description);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
function variantSchema(schema, variants) {
|
|
150
|
+
const unionItems = variants.map(schemaToZod);
|
|
151
|
+
if (unionItems.length === 1) {
|
|
152
|
+
const single = unionItems[0] ?? z.never();
|
|
153
|
+
return withDescription(isNullable(schema) ? single.nullable() : single, schema.description);
|
|
154
|
+
}
|
|
155
|
+
const union = z.union(unionItems);
|
|
156
|
+
return withDescription(isNullable(schema) ? union.nullable() : union, schema.description);
|
|
157
|
+
}
|
|
158
|
+
function describedBaseSchema(schema) {
|
|
159
|
+
const base = baseSchema(schema);
|
|
160
|
+
return withDescription(isNullable(schema) ? base.nullable() : base, schema.description);
|
|
161
|
+
}
|
|
162
|
+
function mergeAllOf(allOf) {
|
|
163
|
+
const merged = {};
|
|
164
|
+
const properties = {};
|
|
165
|
+
const required = [];
|
|
166
|
+
for (const item of allOf) {
|
|
167
|
+
Object.assign(merged, item);
|
|
168
|
+
Object.assign(properties, item.properties);
|
|
169
|
+
required.push(...(item.required ?? []));
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
...merged,
|
|
173
|
+
...(Object.keys(properties).length > 0 ? { properties } : {}),
|
|
174
|
+
...(required.length > 0 ? { required } : {}),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function baseSchema(schema) {
|
|
178
|
+
const types = nonNullTypes(schema);
|
|
179
|
+
if (types.length === 0) {
|
|
180
|
+
return schema.properties ? objectSchema(schema) : z.unknown();
|
|
181
|
+
}
|
|
182
|
+
if (types.length === 1) {
|
|
183
|
+
return scalarSchema(schema, types[0] ?? "unknown");
|
|
184
|
+
}
|
|
185
|
+
return z.union(types.map((type) => scalarSchema(schema, type)));
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Relax a response JSON schema to a **structural, type-only** shape for output
|
|
189
|
+
* validation. The goal is a check that fails loud on a genuinely malformed
|
|
190
|
+
* payload (a field typed as the wrong primitive, `result` that is not an object
|
|
191
|
+
* at all) while never rejecting a well-formed real result.
|
|
192
|
+
*
|
|
193
|
+
* So it keeps each field's base `type` (and `nullable`) but drops value-level
|
|
194
|
+
* constraints — `format` (uuid/date-time), `pattern`, length/range bounds,
|
|
195
|
+
* `enum`, and deep array-item schemas — and makes every object open with no
|
|
196
|
+
* required properties. Those constraints are the false-positive risk: they would
|
|
197
|
+
* 500 a real tool call on a non-v4 uuid, an enum that drifted, or an omitted
|
|
198
|
+
* optional field, and they also break the pagination merge (which drops
|
|
199
|
+
* `nextCursor` and adds a `pagination` block). The trade: array element shapes
|
|
200
|
+
* and formats are not deeply validated; the structural contract still is.
|
|
201
|
+
*/
|
|
202
|
+
function relaxJsonSchema(schema) {
|
|
203
|
+
const relaxed = {};
|
|
204
|
+
if (schema.type !== undefined) {
|
|
205
|
+
relaxed.type = schema.type;
|
|
206
|
+
}
|
|
207
|
+
if (isNullable(schema)) {
|
|
208
|
+
relaxed.nullable = true;
|
|
209
|
+
}
|
|
210
|
+
if (schema.properties) {
|
|
211
|
+
const properties = {};
|
|
212
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
213
|
+
properties[key] = relaxJsonSchema(value);
|
|
214
|
+
}
|
|
215
|
+
relaxed.properties = properties;
|
|
216
|
+
relaxed.required = [];
|
|
217
|
+
relaxed.additionalProperties = true;
|
|
218
|
+
if (relaxed.type === undefined) {
|
|
219
|
+
relaxed.type = "object";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
for (const key of ["anyOf", "oneOf", "allOf"]) {
|
|
223
|
+
const variants = schema[key];
|
|
224
|
+
if (variants) {
|
|
225
|
+
relaxed[key] = variants.map(relaxJsonSchema);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Array items and scalar formats/patterns/ranges/enums are intentionally
|
|
229
|
+
// dropped — see the doc comment.
|
|
230
|
+
return relaxed;
|
|
231
|
+
}
|
|
232
|
+
const untrustedProvenanceSchema = z
|
|
233
|
+
.object({ untrusted: z.literal(true), note: z.string() })
|
|
234
|
+
.loose();
|
|
235
|
+
/**
|
|
236
|
+
* The `structuredContent` shape for a tool: `{ result }`, plus an optional
|
|
237
|
+
* `provenance` marker for operations that carry lead-authored text. `result` is
|
|
238
|
+
* the operation's success-response body, deep-relaxed (see
|
|
239
|
+
* {@link relaxJsonSchema}) so real results validate while malformed ones fail
|
|
240
|
+
* loud.
|
|
241
|
+
*/
|
|
242
|
+
export function openApiToolOutputShape(input) {
|
|
243
|
+
const resultSchema = input.body
|
|
244
|
+
? schemaToZod(relaxJsonSchema(input.body))
|
|
245
|
+
: z.unknown();
|
|
246
|
+
const shape = { result: resultSchema };
|
|
247
|
+
if (input.untrustedContent) {
|
|
248
|
+
shape.provenance = untrustedProvenanceSchema.optional();
|
|
249
|
+
}
|
|
250
|
+
return shape;
|
|
251
|
+
}
|
|
252
|
+
export function openApiToolInputShape(input) {
|
|
253
|
+
const shape = {};
|
|
254
|
+
Object.assign(shape, input.workspaceInput);
|
|
255
|
+
if (input.idempotencyRequired) {
|
|
256
|
+
Object.assign(shape, input.idempotencyInput);
|
|
257
|
+
}
|
|
258
|
+
const pathShape = input.spec?.path ? schemaToZod(input.spec.path) : undefined;
|
|
259
|
+
if (pathShape instanceof z.ZodObject) {
|
|
260
|
+
Object.assign(shape, pathShape.shape);
|
|
261
|
+
}
|
|
262
|
+
if (input.spec?.query) {
|
|
263
|
+
shape.query = schemaToZod(input.spec.query).optional();
|
|
264
|
+
}
|
|
265
|
+
if (input.spec?.body) {
|
|
266
|
+
shape.body = schemaToZod(input.spec.body);
|
|
267
|
+
}
|
|
268
|
+
return shape;
|
|
269
|
+
}
|