@lightsage/mcp 0.4.13 → 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.
Files changed (46) hide show
  1. package/README.md +2 -2
  2. package/api/mcp.js +21 -5
  3. package/bin/mcp-server.js +1242 -397
  4. package/bin/mcp-server.js.map +43 -39
  5. package/esm/funcs/evalRunsList.d.ts +1 -1
  6. package/esm/funcs/evalRunsList.js +1 -1
  7. package/esm/landing-page.js +1 -1
  8. package/esm/lib/config.d.ts +3 -3
  9. package/esm/lib/config.js +3 -3
  10. package/esm/mcp-server/mcp-server.js +1 -1
  11. package/esm/mcp-server/server.js +1 -1
  12. package/esm/mcp-server/tools/evalRunsList.js +1 -1
  13. package/esm/mcp-server/tools/evalRunsList.js.map +1 -1
  14. package/esm/models/createevaltemplaterequest.d.ts +2 -1
  15. package/esm/models/createevaltemplaterequest.d.ts.map +1 -1
  16. package/esm/models/createevaltemplaterequest.js +2 -1
  17. package/esm/models/createevaltemplaterequest.js.map +1 -1
  18. package/esm/models/evaltemplate.d.ts +1 -0
  19. package/esm/models/evaltemplate.d.ts.map +1 -1
  20. package/esm/models/evaltemplate.js +1 -0
  21. package/esm/models/evaltemplate.js.map +1 -1
  22. package/esm/models/updateevaltemplaterequest.d.ts +1 -0
  23. package/esm/models/updateevaltemplaterequest.d.ts.map +1 -1
  24. package/esm/models/updateevaltemplaterequest.js +1 -0
  25. package/esm/models/updateevaltemplaterequest.js.map +1 -1
  26. package/esm/models/v2publictask.d.ts +24 -0
  27. package/esm/models/v2publictask.d.ts.map +1 -1
  28. package/esm/models/v2publictask.js +24 -1
  29. package/esm/models/v2publictask.js.map +1 -1
  30. package/esm/tool-names.js +1 -1
  31. package/esm/tool-names.js.map +1 -1
  32. package/lib/oauth.js +182 -9
  33. package/lib/profile.js +79 -0
  34. package/package.json +7 -4
  35. package/src/funcs/evalRunsList.ts +1 -1
  36. package/src/landing-page.ts +1 -1
  37. package/src/lib/config.ts +3 -3
  38. package/src/mcp-server/mcp-server.ts +1 -1
  39. package/src/mcp-server/server.ts +1 -1
  40. package/src/mcp-server/tools/evalRunsList.ts +1 -1
  41. package/src/models/createevaltemplaterequest.ts +6 -2
  42. package/src/models/evaltemplate.ts +4 -0
  43. package/src/models/updateevaltemplaterequest.ts +2 -0
  44. package/src/models/v2publictask.ts +43 -1
  45. package/src/tool-names.ts +1 -1
  46. 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
- return requestState.toAuth();
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
- /** RFC 6750 challenge header pointing clients at our protected-resource metadata. */
104
- export function wwwAuthenticateHeader(req) {
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
- return `Bearer resource_metadata="${metadata}", scope="${scope}"`;
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 the plaintext ls_cli_ key, or throws on failure.
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 exchangeForLightsageKey(clerkUserId, oauthClientId) {
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 Error(`Key exchange failed (${res.status}): ${text.slice(0, 200)}`);
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.13",
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.0",
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.26.0",
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 jobs created through `POST /v2/eval-runs`, ordered newest first, with lifecycle state and progress.
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,
@@ -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.13/mcp-server.mcpb" download="mcp-server.mcpb" class="action-button header-action" style="display: inline-flex; margin-bottom: 16px;">
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.13",
70
- genVersion: "2.943.0",
71
- userAgent: "speakeasy-sdk/mcp-typescript 0.4.13 2.943.0 1.0.0 @lightsage/mcp",
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;
@@ -22,7 +22,7 @@ const routes = buildRouteMap({
22
22
  export const app = buildApplication(routes, {
23
23
  name: "mcp",
24
24
  versionInfo: {
25
- currentVersion: "0.4.13",
25
+ currentVersion: "0.4.15",
26
26
  },
27
27
  });
28
28
 
@@ -103,7 +103,7 @@ export function createMCPServer(deps: {
103
103
  sizes: ["any"],
104
104
  }],
105
105
  name: "LightsageMcp",
106
- version: "0.4.13",
106
+ version: "0.4.15",
107
107
  });
108
108
 
109
109
  const getClient = deps.getSDK || (() =>
@@ -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 jobs created through \`POST /v2/eval-runs\`, ordered newest first, with lifecycle state and progress.`,
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 configuration before supplying prompt-specific evals.
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 configuration before supplying prompt-specific evals.",
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(
@@ -77,7 +112,10 @@ export const V2PublicTask$zodSchema: z.ZodType<V2PublicTask> =
77
112
  "A free-form source type such as `api_performance`; manually created tasks may not have a source.",
78
113
  ),
79
114
  status: z.string().describe(
80
- "The workflow status. New tasks start as `new`; `added_to_sheet` and `snoozed` tasks are listed only when `status=all`.",
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`.",
116
+ ),
117
+ target_url: z.string().nullable().optional().describe(
118
+ "The page the fix should be made on, when the task targets one.",
81
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.",
@@ -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 jobs created through `POST /v2/eval-runs`, ordered newest first, with lifecycle state and progress."
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",