askell-mcp 0.4.13 → 0.4.14
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/package.json +1 -1
- package/src/client/askell-client.ts +24 -29
- package/src/client/response-formatter.ts +34 -53
- package/src/is-record.ts +3 -0
- package/src/openapi/patch-v1.ts +3 -12
- package/src/tools/analysis.ts +129 -43
- package/src/tools/mutation-gate.ts +8 -15
package/package.json
CHANGED
|
@@ -27,7 +27,27 @@ export class AskellClient {
|
|
|
27
27
|
this.baseUrl = normalizeBaseUrl(config.apiBaseUrl);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
private apiKeyFor(kind: ApiKeyKind | undefined): string {
|
|
31
|
+
const apiKeyKind = kind ?? 'secret';
|
|
32
|
+
const apiKey =
|
|
33
|
+
apiKeyKind === 'public'
|
|
34
|
+
? this.config.publicApiKey
|
|
35
|
+
: this.config.secretApiKey;
|
|
36
|
+
|
|
37
|
+
if (!apiKey) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
apiKeyKind === 'public'
|
|
40
|
+
? 'publicApiKey is not configured'
|
|
41
|
+
: 'secretApiKey is not configured',
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return apiKey;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async request(
|
|
49
|
+
input: AskellRequest,
|
|
50
|
+
): Promise<FormattedResponse & { ok: boolean; status: number }> {
|
|
31
51
|
const method = input.method.toUpperCase();
|
|
32
52
|
const path = normalizeApiPath(input.path);
|
|
33
53
|
const url = new URL(`${this.baseUrl}${path}`);
|
|
@@ -49,19 +69,7 @@ export class AskellClient {
|
|
|
49
69
|
}
|
|
50
70
|
}
|
|
51
71
|
|
|
52
|
-
const
|
|
53
|
-
const apiKey =
|
|
54
|
-
apiKeyKind === 'public'
|
|
55
|
-
? this.config.publicApiKey
|
|
56
|
-
: this.config.secretApiKey;
|
|
57
|
-
|
|
58
|
-
if (!apiKey) {
|
|
59
|
-
throw new Error(
|
|
60
|
-
apiKeyKind === 'public'
|
|
61
|
-
? 'publicApiKey is not configured'
|
|
62
|
-
: 'secretApiKey is not configured',
|
|
63
|
-
);
|
|
64
|
-
}
|
|
72
|
+
const apiKey = this.apiKeyFor(input.apiKeyKind);
|
|
65
73
|
|
|
66
74
|
const started = performance.now();
|
|
67
75
|
const response = await fetch(url, {
|
|
@@ -74,8 +82,7 @@ export class AskellClient {
|
|
|
74
82
|
: {}),
|
|
75
83
|
...input.headers,
|
|
76
84
|
},
|
|
77
|
-
body:
|
|
78
|
-
input.body !== undefined ? JSON.stringify(input.body) : undefined,
|
|
85
|
+
body: input.body !== undefined ? JSON.stringify(input.body) : undefined,
|
|
79
86
|
signal: input.signal,
|
|
80
87
|
});
|
|
81
88
|
|
|
@@ -108,19 +115,7 @@ export class AskellClient {
|
|
|
108
115
|
signal?: AbortSignal;
|
|
109
116
|
}): Promise<FormattedResponse & { ok: boolean; status: number }> {
|
|
110
117
|
const maxPages = input.maxPages ?? 20;
|
|
111
|
-
const
|
|
112
|
-
const apiKey =
|
|
113
|
-
apiKeyKind === 'public'
|
|
114
|
-
? this.config.publicApiKey
|
|
115
|
-
: this.config.secretApiKey;
|
|
116
|
-
|
|
117
|
-
if (!apiKey) {
|
|
118
|
-
throw new Error(
|
|
119
|
-
apiKeyKind === 'public'
|
|
120
|
-
? 'publicApiKey is not configured'
|
|
121
|
-
: 'secretApiKey is not configured',
|
|
122
|
-
);
|
|
123
|
-
}
|
|
118
|
+
const apiKey = this.apiKeyFor(input.apiKeyKind);
|
|
124
119
|
|
|
125
120
|
const collected: unknown[] = [];
|
|
126
121
|
let nextUrl: URL | null = null;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
redactSecretsInText,
|
|
4
|
-
} from './redact.ts';
|
|
1
|
+
import { isRecord } from '../is-record.ts';
|
|
2
|
+
import { redactSensitiveFields, redactSecretsInText } from './redact.ts';
|
|
5
3
|
|
|
6
4
|
export interface FormattedResponse {
|
|
7
5
|
text: string;
|
|
@@ -41,11 +39,8 @@ export function limitText(
|
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
function summarizeListItem(item: unknown): unknown {
|
|
44
|
-
if (
|
|
45
|
-
return item;
|
|
46
|
-
}
|
|
42
|
+
if (!isRecord(item)) return item;
|
|
47
43
|
|
|
48
|
-
const obj = item as Record<string, unknown>;
|
|
49
44
|
const out: Record<string, unknown> = {};
|
|
50
45
|
const scalarKeys = [
|
|
51
46
|
'id',
|
|
@@ -74,13 +69,13 @@ function summarizeListItem(item: unknown): unknown {
|
|
|
74
69
|
] as const;
|
|
75
70
|
|
|
76
71
|
for (const key of scalarKeys) {
|
|
77
|
-
if (key in
|
|
78
|
-
out[key] =
|
|
72
|
+
if (key in item) {
|
|
73
|
+
out[key] = item[key];
|
|
79
74
|
}
|
|
80
75
|
}
|
|
81
76
|
|
|
82
|
-
if (
|
|
83
|
-
const customer =
|
|
77
|
+
if (item.customer && typeof item.customer === 'object') {
|
|
78
|
+
const customer = item.customer as Record<string, unknown>;
|
|
84
79
|
out.customer = {
|
|
85
80
|
id: customer.id,
|
|
86
81
|
customer_reference:
|
|
@@ -88,66 +83,63 @@ function summarizeListItem(item: unknown): unknown {
|
|
|
88
83
|
};
|
|
89
84
|
}
|
|
90
85
|
|
|
91
|
-
if (
|
|
92
|
-
const plan =
|
|
86
|
+
if (item.plan && typeof item.plan === 'object') {
|
|
87
|
+
const plan = item.plan as Record<string, unknown>;
|
|
93
88
|
out.plan = {
|
|
94
89
|
id: plan.id,
|
|
95
90
|
name: plan.name,
|
|
96
91
|
};
|
|
97
92
|
}
|
|
98
93
|
|
|
99
|
-
return Object.keys(out).length > 0 ? out :
|
|
94
|
+
return Object.keys(out).length > 0 ? out : item;
|
|
100
95
|
}
|
|
101
96
|
|
|
102
97
|
/** Tight projection for analytical list queries (dates, plan name, customer). */
|
|
103
98
|
function indexListItem(item: unknown): unknown {
|
|
104
|
-
if (
|
|
105
|
-
return item;
|
|
106
|
-
}
|
|
99
|
+
if (!isRecord(item)) return item;
|
|
107
100
|
|
|
108
|
-
const obj = item as Record<string, unknown>;
|
|
109
101
|
const out: Record<string, unknown> = {};
|
|
110
102
|
|
|
111
|
-
if ('id' in
|
|
112
|
-
out.id =
|
|
103
|
+
if ('id' in item) {
|
|
104
|
+
out.id = item.id;
|
|
113
105
|
}
|
|
114
106
|
|
|
115
|
-
if ('start_date' in
|
|
116
|
-
out.start_date =
|
|
117
|
-
} else if ('created_at' in
|
|
118
|
-
out.created_at =
|
|
107
|
+
if ('start_date' in item) {
|
|
108
|
+
out.start_date = item.start_date;
|
|
109
|
+
} else if ('created_at' in item) {
|
|
110
|
+
out.created_at = item.created_at;
|
|
119
111
|
}
|
|
120
112
|
|
|
121
|
-
if ('ended_at' in
|
|
122
|
-
out.ended_at =
|
|
113
|
+
if ('ended_at' in item && item.ended_at != null) {
|
|
114
|
+
out.ended_at = item.ended_at;
|
|
123
115
|
}
|
|
124
116
|
|
|
125
|
-
const plan =
|
|
117
|
+
const plan = item.plan;
|
|
126
118
|
if (typeof plan === 'string' || typeof plan === 'number') {
|
|
127
119
|
out.plan = plan;
|
|
128
120
|
} else if (plan && typeof plan === 'object' && 'name' in plan) {
|
|
129
121
|
out.plan = (plan as { name: unknown }).name;
|
|
130
|
-
} else if ('name' in
|
|
131
|
-
out.name =
|
|
122
|
+
} else if ('name' in item && typeof item.name === 'string') {
|
|
123
|
+
out.name = item.name;
|
|
132
124
|
}
|
|
133
125
|
|
|
134
|
-
if (typeof
|
|
135
|
-
out.code =
|
|
126
|
+
if (typeof item.code === 'string') {
|
|
127
|
+
out.code = item.code;
|
|
136
128
|
}
|
|
137
129
|
|
|
138
130
|
const customerRef =
|
|
139
|
-
|
|
140
|
-
(
|
|
141
|
-
? ((
|
|
142
|
-
(
|
|
143
|
-
(
|
|
131
|
+
item.customer_reference ??
|
|
132
|
+
(item.customer && typeof item.customer === 'object'
|
|
133
|
+
? ((item.customer as Record<string, unknown>).customer_reference ??
|
|
134
|
+
(item.customer as Record<string, unknown>).reference ??
|
|
135
|
+
(item.customer as Record<string, unknown>).id)
|
|
144
136
|
: undefined);
|
|
145
137
|
if (customerRef !== undefined) {
|
|
146
138
|
out.customer_reference = customerRef;
|
|
147
139
|
}
|
|
148
140
|
|
|
149
|
-
if (typeof
|
|
150
|
-
out.email =
|
|
141
|
+
if (typeof item.email === 'string') {
|
|
142
|
+
out.email = item.email;
|
|
151
143
|
}
|
|
152
144
|
|
|
153
145
|
return Object.keys(out).length > 0 ? out : summarizeListItem(item);
|
|
@@ -172,9 +164,7 @@ function serializeListPayload(
|
|
|
172
164
|
body: items.slice(0, returnedCount),
|
|
173
165
|
};
|
|
174
166
|
|
|
175
|
-
return pretty
|
|
176
|
-
? JSON.stringify(payload, null, 2)
|
|
177
|
-
: JSON.stringify(payload);
|
|
167
|
+
return pretty ? JSON.stringify(payload, null, 2) : JSON.stringify(payload);
|
|
178
168
|
}
|
|
179
169
|
|
|
180
170
|
function maxFittingCount(
|
|
@@ -304,11 +294,7 @@ export function buildBoundedListPayload(input: {
|
|
|
304
294
|
|
|
305
295
|
const text = serializeListPayload(
|
|
306
296
|
status,
|
|
307
|
-
compactMeta(
|
|
308
|
-
meta,
|
|
309
|
-
'index',
|
|
310
|
-
'Response too large; returning metadata only',
|
|
311
|
-
),
|
|
297
|
+
compactMeta(meta, 'index', 'Response too large; returning metadata only'),
|
|
312
298
|
[],
|
|
313
299
|
0,
|
|
314
300
|
true,
|
|
@@ -375,12 +361,7 @@ export function formatApiResponse(
|
|
|
375
361
|
}
|
|
376
362
|
|
|
377
363
|
function pickHeaders(headers: Headers): Record<string, string> {
|
|
378
|
-
const interesting = [
|
|
379
|
-
'content-type',
|
|
380
|
-
'date',
|
|
381
|
-
'x-request-id',
|
|
382
|
-
'retry-after',
|
|
383
|
-
];
|
|
364
|
+
const interesting = ['content-type', 'date', 'x-request-id', 'retry-after'];
|
|
384
365
|
const out: Record<string, string> = {};
|
|
385
366
|
|
|
386
367
|
for (const name of interesting) {
|
package/src/is-record.ts
ADDED
package/src/openapi/patch-v1.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Idempotent: safe to run on an already-patched document.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isRecord } from '../is-record.ts';
|
|
9
|
+
|
|
8
10
|
type JsonSchema = Record<string, unknown>;
|
|
9
11
|
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
10
12
|
|
|
@@ -49,14 +51,7 @@ const RESPONSE_BODIES = [
|
|
|
49
51
|
readonly [string, HttpMethod, string, string]
|
|
50
52
|
>;
|
|
51
53
|
|
|
52
|
-
const HTTP_METHODS = [
|
|
53
|
-
'get',
|
|
54
|
-
'post',
|
|
55
|
-
'put',
|
|
56
|
-
'patch',
|
|
57
|
-
'delete',
|
|
58
|
-
'head',
|
|
59
|
-
] as const;
|
|
54
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head'] as const;
|
|
60
55
|
|
|
61
56
|
const nullableString = (maxLength?: number): JsonSchema => ({
|
|
62
57
|
type: 'string',
|
|
@@ -136,10 +131,6 @@ const CUSTOMER_READ_SCHEMA: JsonSchema = {
|
|
|
136
131
|
},
|
|
137
132
|
};
|
|
138
133
|
|
|
139
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
140
|
-
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
134
|
function dropWebhookCallOperations(doc: OpenApiDocument): void {
|
|
144
135
|
const paths = doc.paths;
|
|
145
136
|
if (!paths) {
|
package/src/tools/analysis.ts
CHANGED
|
@@ -2,21 +2,86 @@ import type { CallToolResult, McpServer } from '@modelcontextprotocol/server';
|
|
|
2
2
|
import * as z from 'zod';
|
|
3
3
|
|
|
4
4
|
import { AskellClient } from '../client/askell-client.ts';
|
|
5
|
+
import { isRecord } from '../is-record.ts';
|
|
5
6
|
|
|
6
7
|
/** Minimal shape of the handler `ctx` param needed here — avoids depending on the SDK's internal context type name. */
|
|
7
8
|
type ToolContext = { mcpReq: { signal: AbortSignal } };
|
|
8
9
|
|
|
10
|
+
type SafeResult = { ok: boolean; data: unknown; error?: string };
|
|
11
|
+
|
|
12
|
+
const ERROR_DETAIL_MAX = 200;
|
|
13
|
+
|
|
14
|
+
function clip(text: string): string {
|
|
15
|
+
const oneLine = text.replace(/\s+/g, ' ').trim();
|
|
16
|
+
if (oneLine.length <= ERROR_DETAIL_MAX) return oneLine;
|
|
17
|
+
|
|
18
|
+
return `${oneLine.slice(0, ERROR_DETAIL_MAX - 1)}…`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function messageList(value: unknown): string | undefined {
|
|
22
|
+
if (typeof value === 'string') {
|
|
23
|
+
const text = clip(value);
|
|
24
|
+
return text.length > 0 ? text : undefined;
|
|
25
|
+
}
|
|
26
|
+
if (
|
|
27
|
+
!Array.isArray(value) ||
|
|
28
|
+
!value.every((item) => typeof item === 'string')
|
|
29
|
+
) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const text = clip(value.filter((item) => item.trim().length > 0).join('; '));
|
|
33
|
+
return text.length > 0 ? text : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* One line for the model. The Askell body stays in `data`.
|
|
38
|
+
* DRF `detail` / `non_field_errors` / flat field errors only — a resource
|
|
39
|
+
* object must not be flattened into the summary.
|
|
40
|
+
*/
|
|
41
|
+
export function summarizeApiFailure(status: number, body: unknown): string {
|
|
42
|
+
const detail = apiErrorDetail(body);
|
|
43
|
+
return detail ? `HTTP ${status}: ${detail}` : `HTTP ${status}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function apiErrorDetail(body: unknown): string | undefined {
|
|
47
|
+
if (typeof body === 'string') return messageList(body);
|
|
48
|
+
if (!isRecord(body)) return undefined;
|
|
49
|
+
|
|
50
|
+
if ('detail' in body) {
|
|
51
|
+
const detail = messageList(body.detail);
|
|
52
|
+
if (detail) return detail;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if ('non_field_errors' in body) {
|
|
56
|
+
const detail = messageList(body.non_field_errors);
|
|
57
|
+
if (detail) return detail;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const parts: string[] = [];
|
|
61
|
+
for (const [key, value] of Object.entries(body)) {
|
|
62
|
+
if (key === 'detail' || key === 'non_field_errors') continue;
|
|
63
|
+
const text = messageList(value);
|
|
64
|
+
if (!text) return undefined;
|
|
65
|
+
parts.push(`${key}: ${text}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return parts.length > 0 ? clip(parts.join('; ')) : undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
9
71
|
async function safeRequest(
|
|
10
72
|
client: AskellClient,
|
|
11
73
|
request: Parameters<AskellClient['request']>[0],
|
|
12
|
-
): Promise<
|
|
74
|
+
): Promise<SafeResult> {
|
|
13
75
|
try {
|
|
14
76
|
const response = await client.request(request);
|
|
15
77
|
const parsed = JSON.parse(response.text) as { body?: unknown };
|
|
78
|
+
const data = parsed.body ?? parsed;
|
|
79
|
+
if (response.ok) return { ok: true, data };
|
|
80
|
+
|
|
16
81
|
return {
|
|
17
|
-
ok:
|
|
18
|
-
data
|
|
19
|
-
error: response.
|
|
82
|
+
ok: false,
|
|
83
|
+
data,
|
|
84
|
+
error: summarizeApiFailure(response.status, data),
|
|
20
85
|
};
|
|
21
86
|
} catch (error) {
|
|
22
87
|
return {
|
|
@@ -27,6 +92,13 @@ async function safeRequest(
|
|
|
27
92
|
}
|
|
28
93
|
}
|
|
29
94
|
|
|
95
|
+
function failedCalls(
|
|
96
|
+
calls: Array<[name: string, result: SafeResult]>,
|
|
97
|
+
): string[] | undefined {
|
|
98
|
+
const names = calls.filter(([, result]) => !result.ok).map(([name]) => name);
|
|
99
|
+
return names.length > 0 ? names : undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
30
102
|
export function registerAnalysisTools(
|
|
31
103
|
server: McpServer,
|
|
32
104
|
client: AskellClient,
|
|
@@ -74,7 +146,8 @@ export function registerAnalysisTools(
|
|
|
74
146
|
content: [
|
|
75
147
|
{
|
|
76
148
|
type: 'text',
|
|
77
|
-
text:
|
|
149
|
+
text:
|
|
150
|
+
error instanceof Error ? error.message : 'Pagination failed',
|
|
78
151
|
},
|
|
79
152
|
],
|
|
80
153
|
isError: true,
|
|
@@ -88,7 +161,7 @@ export function registerAnalysisTools(
|
|
|
88
161
|
{
|
|
89
162
|
title: 'Customer overview (v1)',
|
|
90
163
|
description:
|
|
91
|
-
'Fetch a v1 customer and their v1 subscriptions in one call. Useful for support and billing investigations.',
|
|
164
|
+
'Fetch a v1 customer and their v1 subscriptions in one call. Useful for support and billing investigations. Result is an error when the customer fetch fails; subscriptions are still included. `failures` lists every call that failed.',
|
|
92
165
|
inputSchema: z.object({
|
|
93
166
|
customerReference: z
|
|
94
167
|
.string()
|
|
@@ -99,7 +172,10 @@ export function registerAnalysisTools(
|
|
|
99
172
|
openWorldHint: true,
|
|
100
173
|
},
|
|
101
174
|
},
|
|
102
|
-
async (
|
|
175
|
+
async (
|
|
176
|
+
{ customerReference },
|
|
177
|
+
ctx: ToolContext,
|
|
178
|
+
): Promise<CallToolResult> => {
|
|
103
179
|
const [customer, subscriptions] = await Promise.all([
|
|
104
180
|
safeRequest(client, {
|
|
105
181
|
method: 'GET',
|
|
@@ -113,25 +189,26 @@ export function registerAnalysisTools(
|
|
|
113
189
|
}),
|
|
114
190
|
]);
|
|
115
191
|
|
|
192
|
+
const failures = failedCalls([
|
|
193
|
+
['customer', customer],
|
|
194
|
+
['subscriptions', subscriptions],
|
|
195
|
+
]);
|
|
196
|
+
|
|
116
197
|
const payload = {
|
|
117
198
|
customerReference,
|
|
118
199
|
customer,
|
|
119
200
|
subscriptions,
|
|
201
|
+
...(failures ? { failures } : {}),
|
|
202
|
+
...(!customer.ok
|
|
203
|
+
? {
|
|
204
|
+
hint: 'Verify the customerReference with askell_call GET /customers/ or askell_paginate_all.',
|
|
205
|
+
}
|
|
206
|
+
: {}),
|
|
120
207
|
};
|
|
121
208
|
|
|
122
|
-
const notFoundHint =
|
|
123
|
-
!customer.ok && !subscriptions.ok
|
|
124
|
-
? ' Verify the customerReference with askell_call GET /customers/ or askell_paginate_all.'
|
|
125
|
-
: '';
|
|
126
|
-
|
|
127
209
|
return {
|
|
128
|
-
content: [
|
|
129
|
-
|
|
130
|
-
type: 'text',
|
|
131
|
-
text: JSON.stringify(payload, null, 2) + notFoundHint,
|
|
132
|
-
},
|
|
133
|
-
],
|
|
134
|
-
isError: !customer.ok && !subscriptions.ok,
|
|
210
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
211
|
+
isError: !customer.ok,
|
|
135
212
|
};
|
|
136
213
|
},
|
|
137
214
|
);
|
|
@@ -141,7 +218,7 @@ export function registerAnalysisTools(
|
|
|
141
218
|
{
|
|
142
219
|
title: 'Subscription contract overview (v2)',
|
|
143
220
|
description:
|
|
144
|
-
'Fetch a v2 subscription contract and recent billing runs filtered by contract id. The contract payload includes `discount` (active coupon) when one is applied, `shipping_selection` when shipping was chosen at checkout, and `subscriber_page` (customer-facing management URL, read-only).',
|
|
221
|
+
'Fetch a v2 subscription contract and recent billing runs filtered by contract id. The contract payload includes `discount` (active coupon) when one is applied, `shipping_selection` when shipping was chosen at checkout, and `subscriber_page` (customer-facing management URL, read-only). Result is an error when the contract fetch fails. `failures` lists every call that failed.',
|
|
145
222
|
inputSchema: z.object({
|
|
146
223
|
contractId: z
|
|
147
224
|
.union([z.string().min(1), z.int()])
|
|
@@ -181,23 +258,25 @@ export function registerAnalysisTools(
|
|
|
181
258
|
}),
|
|
182
259
|
]);
|
|
183
260
|
|
|
261
|
+
const failures = failedCalls([
|
|
262
|
+
['contract', contract],
|
|
263
|
+
['billingRuns', billingRuns],
|
|
264
|
+
]);
|
|
265
|
+
|
|
184
266
|
const payload = {
|
|
185
267
|
contractId,
|
|
186
268
|
contract,
|
|
187
269
|
billingRuns,
|
|
270
|
+
...(failures ? { failures } : {}),
|
|
271
|
+
...(!contract.ok
|
|
272
|
+
? {
|
|
273
|
+
hint: 'Verify the contractId with askell_call GET /v2/subscription-contracts/.',
|
|
274
|
+
}
|
|
275
|
+
: {}),
|
|
188
276
|
};
|
|
189
277
|
|
|
190
278
|
return {
|
|
191
|
-
content: [
|
|
192
|
-
{
|
|
193
|
-
type: 'text',
|
|
194
|
-
text:
|
|
195
|
-
JSON.stringify(payload, null, 2) +
|
|
196
|
-
(!contract.ok
|
|
197
|
-
? ' Verify the contractId with askell_call GET /v2/subscription-contracts/.'
|
|
198
|
-
: ''),
|
|
199
|
-
},
|
|
200
|
-
],
|
|
279
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
201
280
|
isError: !contract.ok,
|
|
202
281
|
};
|
|
203
282
|
},
|
|
@@ -208,7 +287,7 @@ export function registerAnalysisTools(
|
|
|
208
287
|
{
|
|
209
288
|
title: 'Billing run triage (v2)',
|
|
210
289
|
description:
|
|
211
|
-
'Fetch a billing run by id with optional related contract context for failure analysis.',
|
|
290
|
+
'Fetch a billing run by id with optional related contract context for failure analysis. Result is an error when the billing run fetch fails. A failed related contract stays in the payload and is listed in `failures`.',
|
|
212
291
|
inputSchema: z.object({
|
|
213
292
|
billingRunId: z
|
|
214
293
|
.union([z.string().min(1), z.int()])
|
|
@@ -216,7 +295,9 @@ export function registerAnalysisTools(
|
|
|
216
295
|
includeContract: z
|
|
217
296
|
.boolean()
|
|
218
297
|
.default(true)
|
|
219
|
-
.describe(
|
|
298
|
+
.describe(
|
|
299
|
+
'Also fetch the related subscription contract when the run has a contract id',
|
|
300
|
+
),
|
|
220
301
|
}),
|
|
221
302
|
annotations: {
|
|
222
303
|
readOnlyHint: true,
|
|
@@ -251,23 +332,26 @@ export function registerAnalysisTools(
|
|
|
251
332
|
}
|
|
252
333
|
}
|
|
253
334
|
|
|
335
|
+
const calls: [string, SafeResult][] = [['billingRun', billingRun]];
|
|
336
|
+
if (contract) {
|
|
337
|
+
calls.push(['contract', contract]);
|
|
338
|
+
}
|
|
339
|
+
const failures = failedCalls(calls);
|
|
340
|
+
|
|
254
341
|
const payload = {
|
|
255
342
|
billingRunId,
|
|
256
343
|
billingRun,
|
|
257
344
|
contract,
|
|
345
|
+
...(failures ? { failures } : {}),
|
|
346
|
+
...(!billingRun.ok
|
|
347
|
+
? {
|
|
348
|
+
hint: 'Verify the billingRunId with askell_call GET /v2/billing-runs/.',
|
|
349
|
+
}
|
|
350
|
+
: {}),
|
|
258
351
|
};
|
|
259
352
|
|
|
260
353
|
return {
|
|
261
|
-
content: [
|
|
262
|
-
{
|
|
263
|
-
type: 'text',
|
|
264
|
-
text:
|
|
265
|
-
JSON.stringify(payload, null, 2) +
|
|
266
|
-
(!billingRun.ok
|
|
267
|
-
? ' Verify the billingRunId with askell_call GET /v2/billing-runs/.'
|
|
268
|
-
: ''),
|
|
269
|
-
},
|
|
270
|
-
],
|
|
354
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
271
355
|
isError: !billingRun.ok,
|
|
272
356
|
};
|
|
273
357
|
},
|
|
@@ -285,7 +369,9 @@ export function registerAnalysisTools(
|
|
|
285
369
|
.positive()
|
|
286
370
|
.max(1000)
|
|
287
371
|
.optional()
|
|
288
|
-
.describe(
|
|
372
|
+
.describe(
|
|
373
|
+
'Page size for GET /webhooks/ (Askell default 10, max 1000)',
|
|
374
|
+
),
|
|
289
375
|
}),
|
|
290
376
|
annotations: {
|
|
291
377
|
readOnlyHint: true,
|
|
@@ -4,13 +4,10 @@ import {
|
|
|
4
4
|
} from '@modelcontextprotocol/server';
|
|
5
5
|
|
|
6
6
|
import type { MutationGate } from '../config.ts';
|
|
7
|
+
import { isRecord } from '../is-record.ts';
|
|
7
8
|
|
|
8
9
|
export type MutationGateDecision = { action: 'execute' } | { action: 'elicit' };
|
|
9
10
|
|
|
10
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
11
|
-
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
11
|
/**
|
|
15
12
|
* Per-request client capabilities (protocol 2026-07-28).
|
|
16
13
|
*
|
|
@@ -28,9 +25,8 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
28
25
|
export function readClientCapabilities(
|
|
29
26
|
envelope: unknown,
|
|
30
27
|
): ClientCapabilities | undefined {
|
|
31
|
-
if (!isRecord(envelope))
|
|
32
|
-
|
|
33
|
-
}
|
|
28
|
+
if (!isRecord(envelope)) return undefined;
|
|
29
|
+
|
|
34
30
|
const value = envelope[CLIENT_CAPABILITIES_META_KEY];
|
|
35
31
|
return isRecord(value) ? (value as ClientCapabilities) : undefined;
|
|
36
32
|
}
|
|
@@ -40,16 +36,13 @@ export function clientSupportsFormElicitation(
|
|
|
40
36
|
capabilities: ClientCapabilities | undefined,
|
|
41
37
|
): boolean {
|
|
42
38
|
const elicitation = capabilities?.elicitation;
|
|
43
|
-
if (elicitation == null)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (elicitation.form != null) {
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
39
|
+
if (elicitation == null) return false;
|
|
40
|
+
if (elicitation.form != null) return true;
|
|
41
|
+
|
|
49
42
|
const keys = Object.keys(elicitation);
|
|
50
|
-
if (elicitation.url != null && keys.every((key) => key === 'url'))
|
|
43
|
+
if (elicitation.url != null && keys.every((key) => key === 'url'))
|
|
51
44
|
return false;
|
|
52
|
-
|
|
45
|
+
|
|
53
46
|
return true;
|
|
54
47
|
}
|
|
55
48
|
|