Skip to content

Commit 4e6711d

Browse files
committed
Set up session caps for free MCP usage
1 parent fac7002 commit 4e6711d

6 files changed

Lines changed: 72 additions & 17 deletions

File tree

src/services/ui-api/api-registry.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,26 @@ export class OperationRegistry {
4040
return true;
4141
})
4242
.map(op => {
43-
if (tier === 'pro') return op.definition;
43+
// Strip internal-only fields from output before sending to
44+
// external clients — they're only used here for augmentation.
45+
const { freeTierNote, sessionLimit, ...definition } = op.definition;
46+
47+
if (tier === 'pro') return definition;
4448

4549
// Augment descriptions for free users so AI agents understand limits
46-
let description = op.definition.description;
47-
const { tiers, sessionLimit } = op.definition;
50+
let description = definition.description;
4851

4952
if (sessionLimit) {
5053
description += `\n\n[Free tier] Limited to ${sessionLimit} calls per session. ` +
5154
'Use account.upgrade to subscribe to Pro for unlimited access.';
5255
}
5356

54-
if (description === op.definition.description) return op.definition;
55-
return { ...op.definition, description };
57+
if (freeTierNote) {
58+
description += `\n\n[Free tier] ${freeTierNote}`;
59+
}
60+
61+
if (description === definition.description) return definition;
62+
return { ...definition, description };
5663
});
5764
}
5865

src/services/ui-api/api-types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ export interface OperationDefinition {
88
category: string;
99
tiers: Array<'free' | 'pro'>;
1010
sessionLimit?: number;
11+
// Appended to the description for free users only — explains a free-tier
12+
// limitation that the operation enforces in its handler (e.g. body size
13+
// caps), so agents understand why responses look the way they do.
14+
freeTierNote?: string;
1115
annotations?: {
1216
readOnlyHint?: boolean;
1317
destructiveHint?: boolean;

src/services/ui-api/operations/event-operations.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,20 @@ import { matchFilters } from '../../../model/filters/filter-matching';
66
import { SelectableSearchFilterClasses } from '../../../model/filters/search-filters';
77
import { EventsStore } from '../../../model/events/events-store';
88

9+
// Free-tier body exports are capped to support exploration but not
10+
// arbitrary export (paid feature).
11+
const FREE_TIER_MAX_BODY_CHARS = 100_000;
12+
913
export function registerEventOperations(
1014
registry: OperationRegistry,
1115
eventsStore: EventsStore,
12-
getEvents: () => ReadonlyArray<CollectedEvent>
16+
getEvents: () => ReadonlyArray<CollectedEvent>,
17+
isPaidUser: () => boolean
1318
): void {
1419
registry.register(eventsListOperation(getEvents));
1520
registry.register(eventsGetOutlineOperation(getEvents));
16-
registry.register(eventsGetRequestBodyOperation(getEvents));
17-
registry.register(eventsGetResponseBodyOperation(getEvents));
21+
registry.register(eventsGetRequestBodyOperation(getEvents, isPaidUser));
22+
registry.register(eventsGetResponseBodyOperation(getEvents, isPaidUser));
1823
registry.register(eventsClearOperation(eventsStore));
1924
}
2025

@@ -171,6 +176,10 @@ function eventsGetOutlineOperation(
171176
'Use events.get-request-body or events.get-response-body to retrieve bodies.',
172177
category: 'events',
173178
tiers: ['free', 'pro'],
179+
sessionLimit: 500,
180+
freeTierNote: 'This operation is limited to 500 calls per session for free users, so ' +
181+
'ensure only the necessary events are queried. Upgrade to Pro (account.upgrade) ' +
182+
'for unlimited access to all features.',
174183
annotations: { readOnlyHint: true },
175184
inputSchema: {
176185
type: 'object',
@@ -218,8 +227,15 @@ function eventsGetOutlineOperation(
218227
};
219228
}
220229

230+
function effectiveMaxLength(requested: number | undefined, isPaid: boolean): number | undefined {
231+
if (isPaid) return requested;
232+
if (requested === undefined) return FREE_TIER_MAX_BODY_CHARS;
233+
return Math.min(requested, FREE_TIER_MAX_BODY_CHARS);
234+
}
235+
221236
function eventsGetRequestBodyOperation(
222-
getEvents: () => ReadonlyArray<CollectedEvent>
237+
getEvents: () => ReadonlyArray<CollectedEvent>,
238+
isPaidUser: () => boolean
223239
): Operation {
224240
return {
225241
definition: {
@@ -228,7 +244,10 @@ function eventsGetRequestBodyOperation(
228244
'Use offset and maxLength to retrieve specific ranges of large bodies.',
229245
category: 'events',
230246
tiers: ['free', 'pro'],
231-
sessionLimit: 50,
247+
sessionLimit: 100,
248+
freeTierNote: `Response bodies are capped at ${FREE_TIER_MAX_BODY_CHARS} ` +
249+
'characters per call. Page through larger bodies with offset, or upgrade ' +
250+
'to Pro (account.upgrade) for full bodies.',
232251
annotations: { readOnlyHint: true },
233252
inputSchema: {
234253
type: 'object',
@@ -243,15 +262,16 @@ function eventsGetRequestBodyOperation(
243262

244263
const body = await serializeBody(lookup.exchange, 'request', {
245264
offset: params.offset as number | undefined,
246-
maxLength: params.maxLength as number | undefined
265+
maxLength: effectiveMaxLength(params.maxLength as number | undefined, isPaidUser())
247266
});
248267
return { success: true, data: body };
249268
}
250269
};
251270
}
252271

253272
function eventsGetResponseBodyOperation(
254-
getEvents: () => ReadonlyArray<CollectedEvent>
273+
getEvents: () => ReadonlyArray<CollectedEvent>,
274+
isPaidUser: () => boolean
255275
): Operation {
256276
return {
257277
definition: {
@@ -260,7 +280,10 @@ function eventsGetResponseBodyOperation(
260280
'Use offset and maxLength to retrieve specific ranges of large bodies.',
261281
category: 'events',
262282
tiers: ['free', 'pro'],
263-
sessionLimit: 50,
283+
sessionLimit: 100,
284+
freeTierNote: `Response bodies are capped at ${FREE_TIER_MAX_BODY_CHARS} ` +
285+
'characters per call. Page through larger bodies with offset, or upgrade ' +
286+
'to Pro (account.upgrade) for full bodies.',
264287
annotations: { readOnlyHint: true },
265288
inputSchema: {
266289
type: 'object',
@@ -275,7 +298,7 @@ function eventsGetResponseBodyOperation(
275298

276299
const body = await serializeBody(lookup.exchange, 'response', {
277300
offset: params.offset as number | undefined,
278-
maxLength: params.maxLength as number | undefined
301+
maxLength: effectiveMaxLength(params.maxLength as number | undefined, isPaidUser())
279302
});
280303
return { success: true, data: body };
281304
}

src/services/ui-api/operations/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export function registerAllOperations(
1919
},
2020
getEvents: () => ReadonlyArray<CollectedEvent>
2121
): void {
22-
registerEventOperations(registry, stores.eventsStore, getEvents);
22+
const isPaidUser = () => stores.accountStore.user.isPaidUser();
23+
registerEventOperations(registry, stores.eventsStore, getEvents, isPaidUser);
2324
registerProxyOperations(registry, stores.proxyStore);
2425
registerInterceptorOperations(registry, stores.interceptorStore);
2526
registerAccountOperations(registry, stores.accountStore);

test/unit/services/api/api-registry.spec.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,33 @@ describe('OperationRegistry', () => {
7474
expect(defs[0].description).to.include('Limited to 10 calls per session');
7575
});
7676

77-
it('should not augment descriptions for free users when there is no session limit', () => {
77+
it('should not augment descriptions for free users when there is no session limit or note', () => {
7878
const registry = new OperationRegistry(() => false);
7979
registry.register(makeOperation('test.op'));
8080

8181
const defs = registry.getDefinitions();
8282
expect(defs[0].description).to.equal('Test operation test.op');
8383
});
84+
85+
it('should append freeTierNote to descriptions for free users', () => {
86+
const registry = new OperationRegistry(() => false);
87+
registry.register(makeOperation('test.op', {
88+
freeTierNote: 'Bodies are capped at 100 chars per call.'
89+
}));
90+
91+
const defs = registry.getDefinitions();
92+
expect(defs[0].description).to.include('[Free tier] Bodies are capped at 100 chars per call.');
93+
});
94+
95+
it('should not append freeTierNote for pro users', () => {
96+
const registry = new OperationRegistry(() => true);
97+
registry.register(makeOperation('test.op', {
98+
freeTierNote: 'Bodies are capped at 100 chars per call.'
99+
}));
100+
101+
const defs = registry.getDefinitions();
102+
expect(defs[0].description).to.equal('Test operation test.op');
103+
});
84104
});
85105

86106
describe('execute()', () => {

test/unit/services/api/event-operations.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe('Event operations', () => {
1414
events = [];
1515
registry = new OperationRegistry(() => true);
1616
const mockEventsStore = { clearInterceptedData: () => {} } as any;
17-
registerEventOperations(registry, mockEventsStore, () => events);
17+
registerEventOperations(registry, mockEventsStore, () => events, () => true);
1818
});
1919

2020
describe('events.list', () => {

0 commit comments

Comments
 (0)