Skip to content

Commit d8b22bd

Browse files
committed
Fix MCP lint boundary casts
1 parent 129980c commit d8b22bd

6 files changed

Lines changed: 143 additions & 44 deletions

File tree

apps/cloud/src/mcp/session-durable-object.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ interface McpModelResumeStub {
109109
) => Promise<McpSessionModelResumeResult>;
110110
}
111111

112+
const toMcpModelResumeStub = (stub: unknown): McpModelResumeStub => stub as McpModelResumeStub;
113+
112114
class OrganizationNotFoundError extends Data.TaggedError("OrganizationNotFoundError")<{
113115
readonly organizationId: string;
114116
}> {}
@@ -211,10 +213,10 @@ export class McpSessionDOSqlite extends McpAgentSessionDOBase<Env, CloudSessionD
211213
): Effect.Effect<McpSessionModelResumeResult, unknown> {
212214
return Effect.tryPromise({
213215
try: () =>
214-
(
216+
toMcpModelResumeStub(
215217
env.MCP_SESSION.get(
216218
env.MCP_SESSION.idFromName(mcpSessionDurableObjectName(owner.sessionId)),
217-
) as unknown as McpModelResumeStub
219+
),
218220
).resumeExecutionForModel(executionId, identity, response),
219221
catch: (cause) => new McpModelResumeForwardError({ cause }),
220222
});

apps/host-cloudflare/src/mcp/session-durable-object.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ interface McpModelResumeStub {
5555
) => Promise<McpSessionModelResumeResult>;
5656
}
5757

58+
const toMcpModelResumeStub = (stub: unknown): McpModelResumeStub => stub as McpModelResumeStub;
59+
5860
class McpModelResumeForwardError extends Data.TaggedError("McpModelResumeForwardError")<{
5961
readonly cause: unknown;
6062
}> {}
@@ -84,10 +86,10 @@ export class McpSessionDO extends McpAgentSessionDOBase<CloudflareEnv, CfSession
8486
): Effect.Effect<McpSessionModelResumeResult, unknown> {
8587
return Effect.tryPromise({
8688
try: () =>
87-
(
89+
toMcpModelResumeStub(
8890
this.cfEnv.MCP_SESSION.get(
8991
this.cfEnv.MCP_SESSION.idFromName(mcpSessionDurableObjectName(owner.sessionId)),
90-
) as unknown as McpModelResumeStub
92+
),
9193
).resumeExecutionForModel(executionId, identity, response),
9294
catch: (cause) => new McpModelResumeForwardError({ cause }),
9395
});

packages/hosts/cloudflare/src/mcp/agent-session-model-resume.test.ts

Lines changed: 110 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -67,20 +67,80 @@ vi.mock("agents/mcp", () => ({
6767
},
6868
}));
6969

70-
class FakeStorage {
70+
class FakeStorage implements DurableObjectStorage {
7171
private readonly values = new Map<string, unknown>();
72+
readonly sql = {} as DurableObjectStorage["sql"];
73+
readonly kv = {} as DurableObjectStorage["kv"];
7274
alarmAt: number | null = null;
7375

74-
async get<T>(key: string): Promise<T | undefined> {
75-
return this.values.get(key) as T | undefined;
76+
async get<T>(key: string): Promise<T | undefined>;
77+
async get<T>(keys: string[]): Promise<Map<string, T>>;
78+
async get<T>(keyOrKeys: string | string[]): Promise<T | undefined | Map<string, T>> {
79+
if (Array.isArray(keyOrKeys)) {
80+
return new Map(keyOrKeys.map((key) => [key, this.values.get(key) as T]));
81+
}
82+
return this.values.get(keyOrKeys) as T | undefined;
83+
}
84+
85+
async put<T>(key: string, value: T): Promise<void>;
86+
async put<T>(entries: Record<string, T> | Map<string, T>): Promise<void>;
87+
async put<T>(
88+
keyOrEntries: string | Record<string, T> | Map<string, T>,
89+
value?: T,
90+
): Promise<void> {
91+
if (typeof keyOrEntries === "string") {
92+
this.values.set(keyOrEntries, value);
93+
return;
94+
}
95+
const entries =
96+
keyOrEntries instanceof Map ? keyOrEntries.entries() : Object.entries(keyOrEntries);
97+
for (const [key, entry] of entries) this.values.set(key, entry);
98+
}
99+
100+
async delete(key: string): Promise<boolean>;
101+
async delete(keys: string[]): Promise<number>;
102+
async delete(keyOrKeys: string | string[]): Promise<boolean | number> {
103+
if (!Array.isArray(keyOrKeys)) return this.values.delete(keyOrKeys);
104+
let deleted = 0;
105+
for (const key of keyOrKeys) {
106+
if (this.values.delete(key)) deleted += 1;
107+
}
108+
return deleted;
109+
}
110+
111+
async list<T = unknown>(): Promise<Map<string, T>> {
112+
return new Map([...this.values.entries()].map(([key, value]) => [key, value as T]));
113+
}
114+
115+
async deleteAll(): Promise<void> {
116+
this.values.clear();
117+
this.alarmAt = null;
118+
}
119+
120+
transaction<T>(_closure: (txn: DurableObjectTransaction) => Promise<T>): Promise<T> {
121+
return Promise.resolve(undefined as T);
122+
}
123+
124+
transactionSync<T>(_closure: () => T): T {
125+
return undefined as T;
126+
}
127+
128+
async sync(): Promise<void> {}
129+
130+
async getAlarm(): Promise<number | null> {
131+
return this.alarmAt;
132+
}
133+
134+
async getCurrentBookmark(): Promise<string> {
135+
return "test-bookmark";
76136
}
77137

78-
async put<T>(key: string, value: T): Promise<void> {
79-
this.values.set(key, value);
138+
async getBookmarkForTime(_timestamp: number | Date): Promise<string> {
139+
return "test-bookmark";
80140
}
81141

82-
async delete(key: string): Promise<boolean> {
83-
return this.values.delete(key);
142+
onNextSessionRestoreBookmark(_bookmark: string): Promise<string> {
143+
return Promise.resolve("test-bookmark");
84144
}
85145

86146
async setAlarm(scheduledTime: number | Date): Promise<void> {
@@ -92,18 +152,21 @@ class FakeStorage {
92152
}
93153
}
94154

95-
class FakeDurableObjectState {
155+
class FakeDurableObjectState implements DurableObjectState {
96156
readonly storage = new FakeStorage();
97157
readonly id: DurableObjectId;
158+
readonly props: unknown = undefined;
159+
readonly facets = {} as DurableObjectState["facets"];
98160
readonly ctx = this;
99161
private waitUntilPromises: Promise<unknown>[] = [];
100162

101163
constructor(name: string) {
102-
this.id = {
164+
const id: Pick<DurableObjectId, "equals" | "name" | "toString"> = {
103165
equals: (other: DurableObjectId) => other.toString() === name,
104166
name,
105167
toString: () => name,
106-
} as unknown as DurableObjectId;
168+
};
169+
this.id = id as DurableObjectId;
107170
}
108171

109172
waitUntil(promise: Promise<unknown>): void {
@@ -120,6 +183,34 @@ class FakeDurableObjectState {
120183
blockConcurrencyWhile<T>(callback: () => Promise<T>): Promise<T> {
121184
return callback();
122185
}
186+
187+
acceptWebSocket(_ws: WebSocket, _tags?: string[]): void {}
188+
189+
getWebSockets(_tag?: string): WebSocket[] {
190+
return [];
191+
}
192+
193+
getTags(_ws: WebSocket): string[] {
194+
return [];
195+
}
196+
197+
setWebSocketAutoResponse(_pair?: WebSocketRequestResponsePair): void {}
198+
199+
getWebSocketAutoResponse(): WebSocketRequestResponsePair | null {
200+
return null;
201+
}
202+
203+
getWebSocketAutoResponseTimestamp(_ws: WebSocket): Date | null {
204+
return null;
205+
}
206+
207+
setHibernatableWebSocketEventTimeout(_timeoutMs?: number): void {}
208+
209+
getHibernatableWebSocketEventTimeout(): number | null {
210+
return null;
211+
}
212+
213+
abort(_reason?: string): void {}
123214
}
124215

125216
class InMemoryExecutionOwnerDirectoryNamespace implements McpExecutionOwnerDirectoryNamespace<string> {
@@ -134,7 +225,7 @@ class InMemoryExecutionOwnerDirectoryNamespace implements McpExecutionOwnerDirec
134225
if (!directory) {
135226
directory = new McpExecutionOwnerDirectoryDO({
136227
storage: new FakeStorage(),
137-
} as unknown as DurableObjectState);
228+
});
138229
this.directories.set(id, directory);
139230
}
140231
return directory;
@@ -200,13 +291,6 @@ type PendingApprovalLeaseSnapshot = {
200291
readonly timeout: ReturnType<typeof setTimeout> | null;
201292
};
202293

203-
type PrivateRuntimeState = {
204-
engine: ExecutionEngine<Cause.YieldableError> | null;
205-
dbHandle: TestDbHandle | null;
206-
initialized: boolean;
207-
pendingApprovalLeases: Map<string, PendingApprovalLeaseSnapshot>;
208-
};
209-
210294
class HarnessSession extends McpAgentSessionDOBase<Cloudflare.Env, TestDbHandle> {
211295
private readonly meta: SessionMeta;
212296
private readonly fakeState: FakeDurableObjectState;
@@ -227,7 +311,7 @@ class HarnessSession extends McpAgentSessionDOBase<Cloudflare.Env, TestDbHandle>
227311
readonly forwardModelResumeToOwner?: HarnessSession["modelResumeForward"];
228312
}) {
229313
const state = new FakeDurableObjectState(input.sessionId);
230-
super(state as unknown as DurableObjectState, {} as Cloudflare.Env);
314+
super(state, {} as Cloudflare.Env);
231315
this.fakeState = state;
232316
this.meta = input.meta ?? sessionMeta();
233317
this.directory = mcpExecutionOwnerDirectoryFromNamespace(input.directoryNamespace);
@@ -269,10 +353,9 @@ class HarnessSession extends McpAgentSessionDOBase<Cloudflare.Env, TestDbHandle>
269353
}
270354

271355
protected override buildMcpServer(): Effect.Effect<BuiltMcpServer> {
272-
const runtime = this.runtimeState();
273356
return Effect.succeed({
274357
mcpServer: new McpServer({ name: "test", version: "1.0.0" }),
275-
engine: runtime.engine!,
358+
engine: this["engine"]!,
276359
});
277360
}
278361

@@ -298,32 +381,25 @@ class HarnessSession extends McpAgentSessionDOBase<Cloudflare.Env, TestDbHandle>
298381
executionId: string,
299382
response: ResumeResponse,
300383
): Promise<McpSessionModelResumeResult | null> {
301-
const local = await Effect.runPromise(
302-
this.runtimeState().engine!.resume(executionId, response),
303-
);
384+
const local = await Effect.runPromise(this["engine"]!.resume(executionId, response));
304385
if (local) return { status: "result", result: formatMcpExecutionOutcome(local) };
305386
return Effect.runPromise(this.modelResumeFallback(executionId, response));
306387
}
307388

308389
pendingLease(executionId: string): PendingApprovalLeaseSnapshot | undefined {
309-
return this.runtimeState().pendingApprovalLeases.get(executionId);
390+
return this["pendingApprovalLeases"].get(executionId);
310391
}
311392

312393
pendingLeaseCount(): number {
313-
return this.runtimeState().pendingApprovalLeases.size;
394+
return this["pendingApprovalLeases"].size;
314395
}
315396

316397
private installRuntime(engine: ExecutionEngine<Cause.YieldableError>): void {
317-
const runtime = this.runtimeState();
318-
runtime.engine = engine;
319-
runtime.dbHandle = { end: () => undefined };
320-
runtime.initialized = true;
398+
this["engine"] = engine;
399+
this["dbHandle"] = { end: () => undefined };
400+
this["initialized"] = true;
321401
this.server = new McpServer({ name: "test", version: "1.0.0" });
322402
}
323-
324-
private runtimeState(): PrivateRuntimeState {
325-
return this as unknown as PrivateRuntimeState;
326-
}
327403
}
328404

329405
const makeDirectory = () => {

packages/hosts/cloudflare/src/mcp/execution-owner-directory.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const makeDirectory = () => {
3434
const storage = new FakeStorage();
3535
const directory = new McpExecutionOwnerDirectoryDO({
3636
storage,
37-
} as unknown as DurableObjectState);
37+
});
3838
return { directory, storage };
3939
};
4040

packages/hosts/cloudflare/src/mcp/execution-owner-directory.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,21 @@ export interface McpExecutionOwnerDirectoryNamespace<Id> {
3030
readonly get: (id: Id) => unknown;
3131
}
3232

33+
interface McpExecutionOwnerDirectoryStorage {
34+
readonly get: <T>(key: string) => Promise<T | undefined>;
35+
readonly put: <T>(key: string, value: T) => Promise<void>;
36+
readonly delete: (key: string) => Promise<boolean>;
37+
readonly setAlarm: (scheduledTime: number | Date) => Promise<void>;
38+
readonly deleteAlarm: () => Promise<void>;
39+
}
40+
41+
interface McpExecutionOwnerDirectoryState {
42+
readonly storage: McpExecutionOwnerDirectoryStorage;
43+
}
44+
45+
const toMcpExecutionOwnerDirectoryStub = (stub: unknown): McpExecutionOwnerDirectoryStub =>
46+
stub as McpExecutionOwnerDirectoryStub;
47+
3348
export const mcpSessionDurableObjectName = (sessionId: string): string =>
3449
`streamable-http:${sessionId}`;
3550

@@ -59,9 +74,9 @@ const isOwnerRecord = (value: unknown): value is McpExecutionOwnerRecord =>
5974
const expiryMs = (record: McpExecutionOwnerRecord): number => Date.parse(record.expiresAt);
6075

6176
export class McpExecutionOwnerDirectoryDO {
62-
private readonly storage: DurableObjectStorage;
77+
private readonly storage: McpExecutionOwnerDirectoryStorage;
6378

64-
constructor(ctx: DurableObjectState) {
79+
constructor(ctx: McpExecutionOwnerDirectoryState) {
6580
this.storage = ctx.storage;
6681
}
6782

@@ -99,7 +114,7 @@ export const mcpExecutionOwnerDirectoryFromNamespace = <Id>(
99114
): McpExecutionOwnerDirectory | null => {
100115
if (!namespace) return null;
101116
const stubFor = (executionId: string): McpExecutionOwnerDirectoryStub =>
102-
namespace.get(namespace.idFromName(executionId)) as unknown as McpExecutionOwnerDirectoryStub;
117+
toMcpExecutionOwnerDirectoryStub(namespace.get(namespace.idFromName(executionId)));
103118
return {
104119
put: (record) =>
105120
Effect.tryPromise({

packages/hosts/mcp/src/tool-server.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ class TestExecutionError extends Data.TaggedError("TestExecutionError")<{
3030
readonly message: string;
3131
}> {}
3232

33+
const expectString: (value: unknown) => asserts value is string = (value) => {
34+
expect(typeof value).toBe("string");
35+
};
36+
3337
const makeStubEngine = <E extends Cause.YieldableError = never>(overrides: {
3438
execute?: ExecutionEngine<E>["execute"];
3539
executeWithPause?: ExecutionEngine<E>["executeWithPause"];
@@ -1478,10 +1482,10 @@ describe("MCP host server — client without elicitation (pause/resume)", () =>
14781482
});
14791483
const after = Date.now();
14801484
const structured = result.structuredContent as Record<string, unknown>;
1481-
const expiresAt = structured.expiresAt as string;
1485+
const expiresAt = structured.expiresAt;
14821486

14831487
expect(structured.ttlMs).toBe(60_000);
1484-
expect(typeof expiresAt).toBe("string");
1488+
expectString(expiresAt);
14851489
const expiresAtMs = Date.parse(expiresAt);
14861490
expect(expiresAtMs).toBeGreaterThanOrEqual(before + 60_000);
14871491
expect(expiresAtMs).toBeLessThanOrEqual(after + 60_000);

0 commit comments

Comments
 (0)