@lightsage/mcp 0.4.14 → 0.4.15
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 +2 -2
- package/api/mcp.js +21 -5
- package/bin/mcp-server.js +1241 -396
- package/bin/mcp-server.js.map +43 -39
- package/esm/funcs/evalRunsList.d.ts +1 -1
- package/esm/funcs/evalRunsList.js +1 -1
- package/esm/landing-page.js +1 -1
- package/esm/lib/config.d.ts +3 -3
- package/esm/lib/config.js +3 -3
- package/esm/mcp-server/mcp-server.js +1 -1
- package/esm/mcp-server/server.js +1 -1
- package/esm/mcp-server/tools/evalRunsList.js +1 -1
- package/esm/mcp-server/tools/evalRunsList.js.map +1 -1
- package/esm/models/createevaltemplaterequest.d.ts +2 -1
- package/esm/models/createevaltemplaterequest.d.ts.map +1 -1
- package/esm/models/createevaltemplaterequest.js +2 -1
- package/esm/models/createevaltemplaterequest.js.map +1 -1
- package/esm/models/evaltemplate.d.ts +1 -0
- package/esm/models/evaltemplate.d.ts.map +1 -1
- package/esm/models/evaltemplate.js +1 -0
- package/esm/models/evaltemplate.js.map +1 -1
- package/esm/models/updateevaltemplaterequest.d.ts +1 -0
- package/esm/models/updateevaltemplaterequest.d.ts.map +1 -1
- package/esm/models/updateevaltemplaterequest.js +1 -0
- package/esm/models/updateevaltemplaterequest.js.map +1 -1
- package/esm/models/v2publictask.d.ts +24 -0
- package/esm/models/v2publictask.d.ts.map +1 -1
- package/esm/models/v2publictask.js +23 -0
- package/esm/models/v2publictask.js.map +1 -1
- package/esm/tool-names.js +1 -1
- package/esm/tool-names.js.map +1 -1
- package/lib/oauth.js +182 -9
- package/lib/profile.js +79 -0
- package/package.json +7 -4
- package/src/funcs/evalRunsList.ts +1 -1
- package/src/landing-page.ts +1 -1
- package/src/lib/config.ts +3 -3
- package/src/mcp-server/mcp-server.ts +1 -1
- package/src/mcp-server/server.ts +1 -1
- package/src/mcp-server/tools/evalRunsList.ts +1 -1
- package/src/models/createevaltemplaterequest.ts +6 -2
- package/src/models/evaltemplate.ts +4 -0
- package/src/models/updateevaltemplaterequest.ts +2 -0
- package/src/models/v2publictask.ts +42 -0
- package/src/tool-names.ts +1 -1
- package/test/mcp-auth.test.js +203 -0
package/lib/oauth.js
CHANGED
|
@@ -65,6 +65,86 @@ export function protectedResourceMetadataUrl(req) {
|
|
|
65
65
|
return `${proto}://${host}/.well-known/oauth-protected-resource/mcp`;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/** Resource identifiers an access token may be audience-bound to. */
|
|
69
|
+
export function acceptedResourceUrls(req) {
|
|
70
|
+
const urls = [
|
|
71
|
+
"https://mcp.lightsage.com/mcp",
|
|
72
|
+
"https://mcp.usesapient.com/mcp",
|
|
73
|
+
process.env.MCP_RESOURCE_URL,
|
|
74
|
+
resourceUrl(req),
|
|
75
|
+
];
|
|
76
|
+
return new Set(urls.filter(Boolean).map(normalizeResourceUrl));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeResourceUrl(value) {
|
|
80
|
+
return String(value).replace(/\/+$/, "");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Origin and slash-trimmed path of a resource URL, or null if it is not one. */
|
|
84
|
+
function parseResourceUrl(value) {
|
|
85
|
+
let url;
|
|
86
|
+
try {
|
|
87
|
+
url = new URL(value);
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
92
|
+
return { origin: url.origin, path: url.pathname.replace(/\/+$/, "") };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Whether an audience names an accepted resource. Clerk copies the client's
|
|
97
|
+
* `resource` parameter into `aud` unchanged, so the same server can arrive as
|
|
98
|
+
* its exact URL, with a trailing slash or default port, or as its origin
|
|
99
|
+
* alone. An audience on another origin, or on another path of this origin,
|
|
100
|
+
* does not match.
|
|
101
|
+
*/
|
|
102
|
+
function audienceNamesResource(audience, accepted) {
|
|
103
|
+
const named = parseResourceUrl(audience);
|
|
104
|
+
const resource = parseResourceUrl(accepted);
|
|
105
|
+
if (!named || !resource || named.origin !== resource.origin) return false;
|
|
106
|
+
return (
|
|
107
|
+
named.path === resource.path || resource.path.startsWith(`${named.path}/`)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Resource URLs a JWT access token names in its `aud` / `resource` claims. */
|
|
112
|
+
function tokenResourceAudiences(token) {
|
|
113
|
+
const segments = token.split(".");
|
|
114
|
+
if (segments.length !== 3) return [];
|
|
115
|
+
let payload;
|
|
116
|
+
try {
|
|
117
|
+
payload = JSON.parse(Buffer.from(segments[1], "base64url").toString("utf8"));
|
|
118
|
+
} catch {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
return [payload?.aud, payload?.resource]
|
|
122
|
+
.flat()
|
|
123
|
+
.filter((value) => typeof value === "string" && /^https?:\/\//i.test(value));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Whether the resource URLs a token names include this MCP server. */
|
|
127
|
+
function audiencesAllowed(audiences, req) {
|
|
128
|
+
if (audiences.length === 0) return true;
|
|
129
|
+
const accepted = [...acceptedResourceUrls(req)];
|
|
130
|
+
return audiences.some((audience) =>
|
|
131
|
+
accepted.some((resource) => audienceNamesResource(audience, resource)),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check that a verified access token was minted for this MCP server.
|
|
137
|
+
*
|
|
138
|
+
* A token that names resource URLs must name this one, so a token issued for
|
|
139
|
+
* another resource server cannot be replayed here. A token that names none is
|
|
140
|
+
* accepted: Clerk only binds a token to a resource when the client sent one,
|
|
141
|
+
* and never for a connection authorized before `aud_claim_enabled` was on.
|
|
142
|
+
* Call only after the signature is verified.
|
|
143
|
+
*/
|
|
144
|
+
export function tokenAudienceAllowed(token, req) {
|
|
145
|
+
return audiencesAllowed(tokenResourceAudiences(token), req);
|
|
146
|
+
}
|
|
147
|
+
|
|
68
148
|
export function oauthAuthenticationOptions() {
|
|
69
149
|
// Do not set `authorizedParties` for OAuth access tokens. Clerk uses the
|
|
70
150
|
// dynamically registered OAuth client ID (for example, Claude's client ID)
|
|
@@ -77,8 +157,10 @@ export function oauthAuthenticationOptions() {
|
|
|
77
157
|
* Verify the incoming Authorization: Bearer OAuth access token with Clerk.
|
|
78
158
|
* Returns the Clerk auth object ({ userId, scopes, clientId, ... }) on success,
|
|
79
159
|
* or null when the request is unauthenticated / the token is invalid.
|
|
160
|
+
*
|
|
161
|
+
* Pass `logger` to record tokens the audience check rejects.
|
|
80
162
|
*/
|
|
81
|
-
export async function verifyOAuthToken(req) {
|
|
163
|
+
export async function verifyOAuthToken(req, logger) {
|
|
82
164
|
const authorization = req.headers["authorization"];
|
|
83
165
|
if (!authorization || !/^bearer\s+/i.test(authorization)) {
|
|
84
166
|
return null;
|
|
@@ -97,22 +179,53 @@ export async function verifyOAuthToken(req) {
|
|
|
97
179
|
if (!requestState.isAuthenticated) {
|
|
98
180
|
return null;
|
|
99
181
|
}
|
|
100
|
-
|
|
182
|
+
const auth = requestState.toAuth();
|
|
183
|
+
const token = authorization.replace(/^bearer\s+/i, "").trim();
|
|
184
|
+
const audiences = tokenResourceAudiences(token);
|
|
185
|
+
if (!audiencesAllowed(audiences, req)) {
|
|
186
|
+
logger?.warning("OAuth token rejected by the audience check", {
|
|
187
|
+
audiences,
|
|
188
|
+
clientId: auth?.clientId,
|
|
189
|
+
});
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
return auth;
|
|
101
193
|
}
|
|
102
194
|
|
|
103
|
-
/**
|
|
104
|
-
|
|
195
|
+
/**
|
|
196
|
+
* RFC 6750 challenge header pointing clients at our protected-resource
|
|
197
|
+
* metadata. Pass `error` / `errorDescription` when a presented token was
|
|
198
|
+
* rejected, so the client knows to run the OAuth flow again.
|
|
199
|
+
*/
|
|
200
|
+
export function wwwAuthenticateHeader(req, { error, errorDescription } = {}) {
|
|
105
201
|
const metadata = protectedResourceMetadataUrl(req);
|
|
106
202
|
const scope = MCP_SUPPORTED_SCOPES.join(" ");
|
|
107
|
-
|
|
203
|
+
const params = [`resource_metadata="${metadata}"`, `scope="${scope}"`];
|
|
204
|
+
if (error) params.push(`error="${error}"`);
|
|
205
|
+
if (errorDescription) {
|
|
206
|
+
params.push(`error_description="${errorDescription.replace(/["\\]/g, "")}"`);
|
|
207
|
+
}
|
|
208
|
+
return `Bearer ${params.join(", ")}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** A key exchange the backend refused, with its machine-readable reason. */
|
|
212
|
+
export class KeyExchangeError extends Error {
|
|
213
|
+
constructor(message, { status, code } = {}) {
|
|
214
|
+
super(message);
|
|
215
|
+
this.name = "KeyExchangeError";
|
|
216
|
+
this.status = status;
|
|
217
|
+
this.code = code;
|
|
218
|
+
}
|
|
108
219
|
}
|
|
109
220
|
|
|
110
221
|
/**
|
|
111
222
|
* Exchange a verified Clerk identity for a Lightsage public-API key.
|
|
112
223
|
* Calls the private backend exchange endpoint with the shared service secret.
|
|
113
|
-
* Returns
|
|
224
|
+
* Returns `{ apiKey, profile }`, where `profile` is the connected Lightsage
|
|
225
|
+
* profile (absent on backends that predate it). Throws KeyExchangeError when
|
|
226
|
+
* the backend refuses the exchange.
|
|
114
227
|
*/
|
|
115
|
-
export async function
|
|
228
|
+
export async function exchangeForLightsageIdentity(clerkUserId, oauthClientId) {
|
|
116
229
|
const baseUrl = process.env.LIGHTSAGE_EXCHANGE_URL;
|
|
117
230
|
const secret = process.env.MCP_EXCHANGE_SECRET;
|
|
118
231
|
if (!baseUrl || !secret || !oauthClientId) {
|
|
@@ -135,14 +248,74 @@ export async function exchangeForLightsageKey(clerkUserId, oauthClientId) {
|
|
|
135
248
|
|
|
136
249
|
if (!res.ok) {
|
|
137
250
|
const text = await res.text().catch(() => "");
|
|
138
|
-
throw new
|
|
251
|
+
throw new KeyExchangeError(
|
|
252
|
+
`Key exchange failed (${res.status}): ${text.slice(0, 200)}`,
|
|
253
|
+
{
|
|
254
|
+
status: res.status,
|
|
255
|
+
code: res.headers.get("x-mcp-exchange-error") || undefined,
|
|
256
|
+
},
|
|
257
|
+
);
|
|
139
258
|
}
|
|
140
259
|
|
|
141
260
|
const data = await res.json();
|
|
142
261
|
if (!data || typeof data.api_key !== "string") {
|
|
143
262
|
throw new Error("Key exchange returned no api_key");
|
|
144
263
|
}
|
|
145
|
-
return data.api_key;
|
|
264
|
+
return { apiKey: data.api_key, profile: data.profile ?? null };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Exchange refusals a new sign-in fixes: the consent page saves the
|
|
268
|
+
// organization this connection uses.
|
|
269
|
+
const REAUTHORIZE_EXCHANGE_ERRORS = new Set([
|
|
270
|
+
"organization_selection_required",
|
|
271
|
+
"not_an_organization_member",
|
|
272
|
+
]);
|
|
273
|
+
// Exchange refusals a new sign-in cannot fix until the account has a workspace.
|
|
274
|
+
const NO_WORKSPACE_EXCHANGE_ERRORS = new Set([
|
|
275
|
+
"user_not_provisioned",
|
|
276
|
+
"no_organization",
|
|
277
|
+
]);
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Map a failed key exchange to the HTTP response the MCP client should get:
|
|
281
|
+
* a re-authorization challenge, an access-denied explanation, or a 502.
|
|
282
|
+
*/
|
|
283
|
+
export function exchangeFailureResponse(req, error) {
|
|
284
|
+
if (REAUTHORIZE_EXCHANGE_ERRORS.has(error?.code)) {
|
|
285
|
+
const errorDescription =
|
|
286
|
+
"Sign in again and choose a Lightsage organization for this connection.";
|
|
287
|
+
return {
|
|
288
|
+
status: 401,
|
|
289
|
+
headers: {
|
|
290
|
+
"WWW-Authenticate": wwwAuthenticateHeader(req, {
|
|
291
|
+
error: "invalid_token",
|
|
292
|
+
errorDescription,
|
|
293
|
+
}),
|
|
294
|
+
},
|
|
295
|
+
body: { error: "invalid_token", error_description: errorDescription },
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
if (NO_WORKSPACE_EXCHANGE_ERRORS.has(error?.code)) {
|
|
299
|
+
return {
|
|
300
|
+
status: 403,
|
|
301
|
+
headers: {},
|
|
302
|
+
body: {
|
|
303
|
+
error: "access_denied",
|
|
304
|
+
error_description:
|
|
305
|
+
"This account has no Lightsage workspace. Sign up at https://app.lightsage.com, then connect again.",
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return { status: 502, headers: {}, body: { error: "key_exchange_failed" } };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Exchange a verified Clerk identity for just the plaintext ls_cli_ key. */
|
|
313
|
+
export async function exchangeForLightsageKey(clerkUserId, oauthClientId) {
|
|
314
|
+
const { apiKey } = await exchangeForLightsageIdentity(
|
|
315
|
+
clerkUserId,
|
|
316
|
+
oauthClientId,
|
|
317
|
+
);
|
|
318
|
+
return apiKey;
|
|
146
319
|
}
|
|
147
320
|
|
|
148
321
|
// ---- Well-known metadata documents ---------------------------------------
|
package/lib/profile.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Authenticated profile tool for OAuth-connected MCP clients.
|
|
2
|
+
//
|
|
3
|
+
// OpenAI hosts (ChatGPT, Codex) call the tool marked `openai/profile` to label
|
|
4
|
+
// a connected account and to recognize the same account after it reconnects.
|
|
5
|
+
// The profile comes from the backend key exchange, which derives a stable
|
|
6
|
+
// opaque id from the Lightsage user and organization behind the access token.
|
|
7
|
+
//
|
|
8
|
+
// Hand-written (not Speakeasy-generated) so regeneration does not remove it.
|
|
9
|
+
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
|
|
12
|
+
export const PROFILE_TOOL_NAME = "get_profile";
|
|
13
|
+
|
|
14
|
+
const profileOutputSchema = z.strictObject({
|
|
15
|
+
id: z.string().min(1).regex(/\S/).describe(
|
|
16
|
+
"Opaque profile identifier, unique within Lightsage and unchanged across token refresh, reconnection, and display-metadata changes. Never reassigned to another profile.",
|
|
17
|
+
),
|
|
18
|
+
email: z.string().optional().describe(
|
|
19
|
+
"Email address for display; not used as the profile identity.",
|
|
20
|
+
),
|
|
21
|
+
nickname: z.string().optional().describe(
|
|
22
|
+
"The Lightsage organization this connection can access.",
|
|
23
|
+
),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/** Build the profile tool result, or an error when no stable id is available. */
|
|
27
|
+
export function profileToolResult(profile) {
|
|
28
|
+
const id = typeof profile?.id === "string" ? profile.id.trim() : "";
|
|
29
|
+
if (!id) {
|
|
30
|
+
return {
|
|
31
|
+
isError: true,
|
|
32
|
+
content: [{ type: "text", text: "Profile identity unavailable." }],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const result = {
|
|
37
|
+
id,
|
|
38
|
+
...(typeof profile.email === "string" && profile.email
|
|
39
|
+
? { email: profile.email }
|
|
40
|
+
: {}),
|
|
41
|
+
...(typeof profile.nickname === "string" && profile.nickname
|
|
42
|
+
? { nickname: profile.nickname }
|
|
43
|
+
: {}),
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
isError: false,
|
|
47
|
+
structuredContent: result,
|
|
48
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Register the profile tool for the profile behind this request's token. */
|
|
53
|
+
export function registerProfileTool(mcpServer, profile) {
|
|
54
|
+
const securitySchemes = [{ type: "oauth2", scopes: [] }];
|
|
55
|
+
mcpServer.registerTool(
|
|
56
|
+
PROFILE_TOOL_NAME,
|
|
57
|
+
{
|
|
58
|
+
title: "Get Profile",
|
|
59
|
+
description:
|
|
60
|
+
"Return the Lightsage profile represented by this request's authenticated credentials. The opaque id is unique within Lightsage and remains unchanged across token refresh, reconnection, and display-metadata changes.",
|
|
61
|
+
inputSchema: z.strictObject({}),
|
|
62
|
+
outputSchema: profileOutputSchema,
|
|
63
|
+
annotations: {
|
|
64
|
+
title: "Get Profile",
|
|
65
|
+
readOnlyHint: true,
|
|
66
|
+
destructiveHint: false,
|
|
67
|
+
idempotentHint: true,
|
|
68
|
+
openWorldHint: false,
|
|
69
|
+
},
|
|
70
|
+
_meta: {
|
|
71
|
+
"openai/profile": true,
|
|
72
|
+
// The MCP SDK drops unknown top-level descriptor fields, so declare
|
|
73
|
+
// the auth policy through the `_meta` mirror OpenAI hosts also read.
|
|
74
|
+
securitySchemes,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
async () => profileToolResult(profile),
|
|
78
|
+
);
|
|
79
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightsage/mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.15",
|
|
4
4
|
"author": "Speakeasy",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@clerk/backend": "^3.11.4",
|
|
25
25
|
"@clerk/mcp-tools": "^0.5.0",
|
|
26
|
-
"@modelcontextprotocol/sdk": "1.
|
|
26
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
27
27
|
"@stricli/core": "^1.1.2",
|
|
28
28
|
"express": "^5.1.0",
|
|
29
29
|
"zod": "^4.0.0"
|
|
30
30
|
},
|
|
31
31
|
"overrides": {
|
|
32
|
-
"@modelcontextprotocol/sdk": "1.
|
|
32
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
33
33
|
"hono": ">=4.13.7 <5.0.0",
|
|
34
34
|
"fast-uri": ">=4.1.3 <5.0.0",
|
|
35
35
|
"js-yaml": ">=4.3.2 <5.0.0",
|
|
@@ -40,8 +40,10 @@
|
|
|
40
40
|
"@hono/node-server": ">=1.19.15 <2.0.0",
|
|
41
41
|
"brace-expansion@^1.1.7": ">=1.1.18 <2.0.0",
|
|
42
42
|
"brace-expansion@^5.0.0": ">=5.0.9 <6.0.0",
|
|
43
|
+
"proxy-addr": ">=2.0.8 <3.0.0",
|
|
43
44
|
"qs": ">=6.16.0 <7.0.0",
|
|
44
|
-
"tmp": ">=0.2.6 <0.3.0"
|
|
45
|
+
"tmp": ">=0.2.6 <0.3.0",
|
|
46
|
+
"node-forge": "$node-forge"
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
|
47
49
|
"@anthropic-ai/mcpb": "^1.2.0",
|
|
@@ -52,6 +54,7 @@
|
|
|
52
54
|
"bun": "^1.2.12",
|
|
53
55
|
"eslint": "^9.26.0",
|
|
54
56
|
"globals": "^16.0.0",
|
|
57
|
+
"node-forge": "file:vendor/node-forge",
|
|
55
58
|
"typescript": "~5.8.3",
|
|
56
59
|
"typescript-eslint": "^8.31.1"
|
|
57
60
|
}
|
|
@@ -30,7 +30,7 @@ import { Result } from "../types/fp.js";
|
|
|
30
30
|
* List Runs
|
|
31
31
|
*
|
|
32
32
|
* @remarks
|
|
33
|
-
* Returns
|
|
33
|
+
* Returns custom eval runs, ordered newest first, with lifecycle state and progress. Includes scheduled, onboarding, and pull request runs and runs started in the Lightsage app, as well as runs started through `POST /v2/eval-runs`.
|
|
34
34
|
*/
|
|
35
35
|
export function evalRunsList(
|
|
36
36
|
client$: LightsageMcpCore,
|
package/src/landing-page.ts
CHANGED
|
@@ -931,7 +931,7 @@ http_headers = { "lightsage-api-key-auth" = "YOUR_LIGHTSAGE_API_KEY_AUTH" }`;
|
|
|
931
931
|
<h1>Instructions</h1>
|
|
932
932
|
<p>One-click installation for Claude Desktop users</p>
|
|
933
933
|
<div class="instruction-item">
|
|
934
|
-
<a href="https://github.com/lightsagehq/mcp/releases/download/v0.4.
|
|
934
|
+
<a href="https://github.com/lightsagehq/mcp/releases/download/v0.4.15/mcp-server.mcpb" download="mcp-server.mcpb" class="action-button header-action" style="display: inline-flex; margin-bottom: 16px;">
|
|
935
935
|
📥 Download MCP Bundle
|
|
936
936
|
</a>
|
|
937
937
|
</div>
|
package/src/lib/config.ts
CHANGED
|
@@ -66,7 +66,7 @@ export function serverURLFromOptions(options: SDKOptions): URL | null {
|
|
|
66
66
|
export const SDK_METADATA = {
|
|
67
67
|
language: "typescript",
|
|
68
68
|
openapiDocVersion: "1.0.0",
|
|
69
|
-
sdkVersion: "0.4.
|
|
70
|
-
genVersion: "2.
|
|
71
|
-
userAgent: "speakeasy-sdk/mcp-typescript 0.4.
|
|
69
|
+
sdkVersion: "0.4.15",
|
|
70
|
+
genVersion: "2.948.4",
|
|
71
|
+
userAgent: "speakeasy-sdk/mcp-typescript 0.4.15 2.948.4 1.0.0 @lightsage/mcp",
|
|
72
72
|
} as const;
|
package/src/mcp-server/server.ts
CHANGED
|
@@ -15,7 +15,7 @@ export const tool$evalRunsList: ToolDefinition<typeof args> = {
|
|
|
15
15
|
name: "eval-runs-list",
|
|
16
16
|
description: `List Runs
|
|
17
17
|
|
|
18
|
-
Returns
|
|
18
|
+
Returns custom eval runs, ordered newest first, with lifecycle state and progress. Includes scheduled, onboarding, and pull request runs and runs started in the Lightsage app, as well as runs started through \`POST /v2/eval-runs\`.`,
|
|
19
19
|
annotations: {
|
|
20
20
|
"title": "",
|
|
21
21
|
"destructiveHint": false,
|
|
@@ -83,11 +83,12 @@ export const CreateEvalTemplateRequestIncludedComponent$zodSchema = z.enum([
|
|
|
83
83
|
]);
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
|
-
* Create reusable
|
|
86
|
+
* Create a reusable eval that can be applied to many brands or prompts.
|
|
87
87
|
*/
|
|
88
88
|
export type CreateEvalTemplateRequest = {
|
|
89
89
|
name: string;
|
|
90
90
|
description?: string | undefined;
|
|
91
|
+
prompt?: string | undefined;
|
|
91
92
|
eval_type?: CreateEvalTemplateRequestEvalType | undefined;
|
|
92
93
|
included_components: Array<CreateEvalTemplateRequestIncludedComponent>;
|
|
93
94
|
persona?: string | undefined;
|
|
@@ -131,6 +132,9 @@ export const CreateEvalTemplateRequest$zodSchema: z.ZodType<
|
|
|
131
132
|
persona: z.string().optional().describe(
|
|
132
133
|
"Optional persona copied when `persona` is included.",
|
|
133
134
|
),
|
|
135
|
+
prompt: z.string().optional().describe(
|
|
136
|
+
"Prompt filled into the new eval form when the template is applied, or the scenario for Agent Preference, where `{{\"{{\"}}competitor_name}}` names the brand each comparison is against. Requests that create evals from a template still supply their own prompts.",
|
|
137
|
+
),
|
|
134
138
|
runs_per_execution: z.int().default(1).describe(
|
|
135
139
|
"Attempt count copied when `runs` is included.",
|
|
136
140
|
),
|
|
@@ -147,5 +151,5 @@ export const CreateEvalTemplateRequest$zodSchema: z.ZodType<
|
|
|
147
151
|
"Runnable target keys copied when `targets` is included, such as `agent:codex:gpt-5.6-terra`.",
|
|
148
152
|
),
|
|
149
153
|
}).describe(
|
|
150
|
-
"Create reusable
|
|
154
|
+
"Create a reusable eval that can be applied to many brands or prompts.",
|
|
151
155
|
);
|
|
@@ -83,6 +83,7 @@ export const EvalTemplateIncludedComponent$zodSchema = z.enum([
|
|
|
83
83
|
export type EvalTemplate = {
|
|
84
84
|
name: string;
|
|
85
85
|
description?: string | null | undefined;
|
|
86
|
+
prompt?: string | null | undefined;
|
|
86
87
|
eval_type?: EvalTemplateEvalType | undefined;
|
|
87
88
|
included_components: Array<EvalTemplateIncludedComponent>;
|
|
88
89
|
persona?: string | null | undefined;
|
|
@@ -134,6 +135,9 @@ export const EvalTemplate$zodSchema: z.ZodType<EvalTemplate> = z.object({
|
|
|
134
135
|
persona: z.string().nullable().optional().describe(
|
|
135
136
|
"Optional persona copied when `persona` is included.",
|
|
136
137
|
),
|
|
138
|
+
prompt: z.string().nullable().optional().describe(
|
|
139
|
+
"Prompt filled into the new eval form when the template is applied, or the scenario for Agent Preference, where `{{\"{{\"}}competitor_name}}` names the brand each comparison is against. Requests that create evals from a template still supply their own prompts.",
|
|
140
|
+
),
|
|
137
141
|
runs_per_execution: z.int().default(1).describe(
|
|
138
142
|
"Attempt count copied when `runs` is included.",
|
|
139
143
|
),
|
|
@@ -78,6 +78,7 @@ export type UpdateEvalTemplateRequest = {
|
|
|
78
78
|
expected_template_version?: number | undefined;
|
|
79
79
|
name?: string | undefined;
|
|
80
80
|
description?: string | undefined;
|
|
81
|
+
prompt?: string | undefined;
|
|
81
82
|
eval_type?: UpdateEvalTemplateRequestEvalType | undefined;
|
|
82
83
|
included_components?:
|
|
83
84
|
| Array<UpdateEvalTemplateRequestIncludedComponent>
|
|
@@ -108,6 +109,7 @@ export const UpdateEvalTemplateRequest$zodSchema: z.ZodType<
|
|
|
108
109
|
judges: z.array(GraderDefinition$zodSchema).nullable().optional(),
|
|
109
110
|
name: z.string().optional(),
|
|
110
111
|
persona: z.string().optional(),
|
|
112
|
+
prompt: z.string().optional(),
|
|
111
113
|
runs_per_execution: z.int().optional(),
|
|
112
114
|
runtime: EvalTemplateRuntime$zodSchema.nullable().optional(),
|
|
113
115
|
schedule: EvalTemplateSchedule$zodSchema.nullable().optional(),
|
|
@@ -5,6 +5,28 @@
|
|
|
5
5
|
|
|
6
6
|
import * as z from "zod";
|
|
7
7
|
import { collectExtraKeys as collectExtraKeys$ } from "../lib/schemas.js";
|
|
8
|
+
import { ClosedEnum } from "../types/enums.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Progress of the Markdown write-up in `content`.
|
|
12
|
+
*/
|
|
13
|
+
export const ContentStatus = {
|
|
14
|
+
None: "none",
|
|
15
|
+
Writing: "writing",
|
|
16
|
+
Ready: "ready",
|
|
17
|
+
Failed: "failed",
|
|
18
|
+
} as const;
|
|
19
|
+
/**
|
|
20
|
+
* Progress of the Markdown write-up in `content`.
|
|
21
|
+
*/
|
|
22
|
+
export type ContentStatus = ClosedEnum<typeof ContentStatus>;
|
|
23
|
+
|
|
24
|
+
export const ContentStatus$zodSchema = z.enum([
|
|
25
|
+
"none",
|
|
26
|
+
"writing",
|
|
27
|
+
"ready",
|
|
28
|
+
"failed",
|
|
29
|
+
]).describe("Progress of the Markdown write-up in `content`.");
|
|
8
30
|
|
|
9
31
|
export type V2PublicTask = {
|
|
10
32
|
id: string;
|
|
@@ -18,6 +40,10 @@ export type V2PublicTask = {
|
|
|
18
40
|
next_action: string;
|
|
19
41
|
description?: string | null | undefined;
|
|
20
42
|
execution_prompt: string;
|
|
43
|
+
content?: string | null | undefined;
|
|
44
|
+
target_url?: string | null | undefined;
|
|
45
|
+
content_status?: ContentStatus | undefined;
|
|
46
|
+
content_written_at?: string | null | undefined;
|
|
21
47
|
reasoning?: string | null | undefined;
|
|
22
48
|
evidence?: { [k: string]: any } | undefined;
|
|
23
49
|
score?: number | null | undefined;
|
|
@@ -43,6 +69,15 @@ export const V2PublicTask$zodSchema: z.ZodType<V2PublicTask> =
|
|
|
43
69
|
action_type: z.string().nullable().optional(),
|
|
44
70
|
assignee_user_id: z.string().nullable().optional(),
|
|
45
71
|
batch_number: z.int(),
|
|
72
|
+
content: z.string().nullable().optional().describe(
|
|
73
|
+
"The task's Markdown write-up covering what went wrong, how to fix it, the impact, and the evidence. Null until the write-up is generated; see `content_status`.",
|
|
74
|
+
),
|
|
75
|
+
content_status: ContentStatus$zodSchema.default("none").describe(
|
|
76
|
+
"Progress of the Markdown write-up in `content`.",
|
|
77
|
+
),
|
|
78
|
+
content_written_at: z.string().nullable().optional().describe(
|
|
79
|
+
"When the write-up in `content` was last generated.",
|
|
80
|
+
),
|
|
46
81
|
created_at: z.string().nullable().optional(),
|
|
47
82
|
description: z.string().nullable().optional(),
|
|
48
83
|
effort: z.string().describe(
|
|
@@ -79,6 +114,9 @@ export const V2PublicTask$zodSchema: z.ZodType<V2PublicTask> =
|
|
|
79
114
|
status: z.string().describe(
|
|
80
115
|
"The workflow status. New tasks start as `new`. A task is `in_review` while its content waits to go live, as an open pull request or a CMS draft; it is listed with `in_progress` tasks. `added_to_sheet` and `snoozed` tasks are listed only when `status=all`.",
|
|
81
116
|
),
|
|
117
|
+
target_url: z.string().nullable().optional().describe(
|
|
118
|
+
"The page the fix should be made on, when the task targets one.",
|
|
119
|
+
),
|
|
82
120
|
task_type: z.string().describe(
|
|
83
121
|
"A free-form label such as `Redirect` or `Docs`; this field is not an enum.",
|
|
84
122
|
),
|
|
@@ -96,6 +134,9 @@ export const V2PublicTask$zodSchemaOutbound: z.ZodType<any> = z.object({
|
|
|
96
134
|
action_type: z.nullable(z.string()).optional(),
|
|
97
135
|
assignee_user_id: z.nullable(z.string()).optional(),
|
|
98
136
|
batch_number: z.int(),
|
|
137
|
+
content: z.nullable(z.string()).optional(),
|
|
138
|
+
content_status: ContentStatus$zodSchema.default("none"),
|
|
139
|
+
content_written_at: z.nullable(z.string()).optional(),
|
|
99
140
|
created_at: z.nullable(z.string()).optional(),
|
|
100
141
|
description: z.nullable(z.string()).optional(),
|
|
101
142
|
effort: z.string(),
|
|
@@ -114,6 +155,7 @@ export const V2PublicTask$zodSchemaOutbound: z.ZodType<any> = z.object({
|
|
|
114
155
|
source_refs: z.record(z.string(), z.any()).optional(),
|
|
115
156
|
source_type: z.nullable(z.string()).optional(),
|
|
116
157
|
status: z.string(),
|
|
158
|
+
target_url: z.nullable(z.string()).optional(),
|
|
117
159
|
task_type: z.string(),
|
|
118
160
|
title: z.string(),
|
|
119
161
|
updated_at: z.nullable(z.string()).optional(),
|
package/src/tool-names.ts
CHANGED
|
@@ -186,7 +186,7 @@ export const toolNames: Array<{ name: string; description: string }>= [
|
|
|
186
186
|
},
|
|
187
187
|
{
|
|
188
188
|
"name": "eval-runs-list",
|
|
189
|
-
"description": "List Runs\n\nReturns
|
|
189
|
+
"description": "List Runs\n\nReturns custom eval runs, ordered newest first, with lifecycle state and progress. Includes scheduled, onboarding, and pull request runs and runs started in the Lightsage app, as well as runs started through `POST /v2/eval-runs`."
|
|
190
190
|
},
|
|
191
191
|
{
|
|
192
192
|
"name": "agent-preference-competitors-list",
|