Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
2f092e3
phase 2 implementation base API
livelifelively Jan 14, 2026
145dc01
base API implementation and tests;
livelifelively Jan 14, 2026
83b2728
fix code smells;
livelifelively Jan 14, 2026
6825f2d
uuid package install
livelifelively Jan 14, 2026
dabe6b4
code smell and uuid
livelifelively Jan 14, 2026
d59dfda
task 2 tests done;
livelifelively Jan 14, 2026
049b97d
fix errors linting;
livelifelively Jan 14, 2026
f49edd4
phase 2 task 2;
livelifelively Jan 14, 2026
4d1448a
Merge branch 'phase-2-impl' of github.com:context-engine/say2 into ph…
livelifelively Jan 14, 2026
4233ad7
Merge branch 'phase-2-tests' of github.com:context-engine/say2 into p…
livelifelively Jan 14, 2026
68372e7
test(mcp): add missing Task 02 integration tests for tool execution
livelifelively Jan 14, 2026
6cffbe4
fix errors
livelifelively Jan 14, 2026
e2c97ce
feat(mcp): implement progress tracking for tool operations
livelifelively Jan 14, 2026
b3ac4c8
task 04 tests done: cancellation schema, unit, and integration tests
livelifelively Jan 14, 2026
a9bf06b
Merge phase-2-tests into phase-2-impl
livelifelively Jan 14, 2026
f94589f
fix ts errors;
livelifelively Jan 14, 2026
0857843
task 05 tests done: content parsing unit and integration tests
livelifelively Jan 14, 2026
3728d4f
task 04 implementation
livelifelively Jan 14, 2026
e727927
Merge branch 'phase-2-impl' of github.com:context-engine/say2 into ph…
livelifelively Jan 14, 2026
914aa91
fix: add setNotificationHandler mock and pass structuredContent throu…
livelifelively Jan 14, 2026
5f5cbc2
feat(mcp): implement Task 05 content parsing with ContentParser and Ajv
livelifelively Jan 14, 2026
a36de43
Merge phase-2-tests into phase-2-impl, resolve parser.test.ts conflict
livelifelively Jan 14, 2026
3d233b4
implement updated specs;
livelifelively Jan 15, 2026
be78229
implement updated specs;
livelifelively Jan 15, 2026
8eca099
test(mcp): add Task 02 Basic Execution schema tests
livelifelively Jan 15, 2026
e137f33
implement gaps;
livelifelively Jan 15, 2026
fb61d1d
Merge branch 'phase-2-impl' of github.com:context-engine/say2 into ph…
livelifelively Jan 15, 2026
d8800fc
test(mcp): fix Task 03 Progress Tracking and Task 04 Cancellation tests
livelifelively Jan 15, 2026
96e0ae3
test(mcp): strengthen unknown token test to verify store not updated
livelifelively Jan 15, 2026
42bf01e
test(mcp): add Task 05 Content Parsing tests
livelifelively Jan 15, 2026
a1849a8
feat(mcp): implement Task 05 content parsing and type refactor
livelifelively Jan 15, 2026
929eb76
Merge branch 'phase-2-impl' of github.com:context-engine/say2 into ph…
livelifelively Jan 15, 2026
99c925f
test(mcp): align Task 05 tests with implementation
livelifelively Jan 15, 2026
33722dc
feat(mcp): integrate ContentParser in callTool()
livelifelively Jan 15, 2026
476db47
test(mcp): add gap-detection tests for contentParser integration
livelifelively Jan 15, 2026
46148fc
Merge branch 'phase-2-impl' of github.com:context-engine/say2 into ph…
livelifelively Jan 15, 2026
79e811c
feat(mcp): add structured output validation in callTool()
livelifelively Jan 15, 2026
e871fbf
Merge branch 'phase-2-impl' of github.com:context-engine/say2 into ph…
livelifelively Jan 15, 2026
1eff422
test(mcp): enable all gap-detection tests after implementation update
livelifelively Jan 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"@biomejs/biome": "^2.3.11",
"@stryker-mutator/core": "^9.4.0",
"@stryker-mutator/typescript-checker": "^9.4.0",
"@types/bun": "^1.3.5",
"@types/bun": "^1.3.6",
"fast-check": "^4.5.3"
}
}
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"test": "bun test"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.1",
"@modelcontextprotocol/sdk": "^1.25.2",
"koa-compose": "^4.1.0",
"xstate": "^5.25.0",
"zod": "^4.3.5"
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/middleware/state-machine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ const mockDetector = {
"method" in msg && msg.method === "notifications/initialized",
extractCapabilities: (msg: JsonRpcMessage) =>
"result" in msg && typeof msg.result === "object" && msg.result !== null
? (msg.result as any).capabilities
? // biome-ignore lint/suspicious/noExplicitAny: dynamic result object
(msg.result as any).capabilities
: undefined,
extractServerInfo: (msg: JsonRpcMessage) =>
"result" in msg && typeof msg.result === "object" && msg.result !== null
? (msg.result as any).serverInfo
? // biome-ignore lint/suspicious/noExplicitAny: dynamic result object
(msg.result as any).serverInfo
: undefined,
};

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/middleware/state-machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export function createStateMachineMiddleware(
sessionManager: SessionManager,
detector: ProtocolDetector,
): Middleware {
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Protocol detection requires sequential checks
return async (ctx: MiddlewareContext, next: NextFn) => {
const { event, session } = ctx;
const payload = event.payload;
Expand Down
28 changes: 21 additions & 7 deletions packages/core/src/middleware/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,13 @@ describe("StoreMiddleware", () => {
session,
extensions: new Map(),
get: () => undefined,
set: () => {},
set: () => {
/* no-op */
},
};
await middleware(ctx, async () => {});
await middleware(ctx, async () => {
/* no-op */
});
} catch (e) {
if ((e as Error).message.includes("Not implemented")) {
// Expected
Expand Down Expand Up @@ -219,7 +223,9 @@ describe("StoreMiddleware", () => {
session,
extensions: new Map(),
get: () => undefined,
set: () => {},
set: () => {
/* no-op */
},
};

await middleware(ctx, async () => {
Expand Down Expand Up @@ -302,18 +308,26 @@ describe("StoreMiddleware", () => {
event: event1,
session,
get: () => undefined,
set: () => {},
set: () => {
/* no-op */
},
},
async () => {
/* no-op */
},
async () => {},
);
await middleware(
{
event: event2,
session: session2,
get: () => undefined,
set: () => {},
set: () => {
/* no-op */
},
},
async () => {
/* no-op */
},
async () => {},
);

const session1Events = store.getBySession(session.id);
Expand Down
12 changes: 8 additions & 4 deletions packages/mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@
"typecheck": "bunx tsc --noEmit"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.2",
"@say2/core": "workspace:*",
"@modelcontextprotocol/sdk": "^1.0.0"
"ajv": "^8.17.1",
"uuid": "^13.0.0",
"zod": "^4.3.5"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5.0.0"
"@types/bun": "^1.3.6",
"@types/uuid": "^11.0.0",
"typescript": "^5.9.3"
}
}
}
118 changes: 118 additions & 0 deletions packages/mcp/src/cancel/manager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { beforeEach, describe, expect, mock, test } from "bun:test";
import { randomUUID } from "node:crypto";
import { CancellationManager } from "./manager";

describe("CancellationManager", () => {
let manager: CancellationManager;
let mockClient: any;

beforeEach(() => {
manager = new CancellationManager();

// Mock MCP client with notification method
mockClient = {
notification: mock(() => Promise.resolve()),
};
manager.setClient(mockClient);
});

test("register() starts timeout timer", () => {
const originalSetTimeout = global.setTimeout;
const setTimeoutMock = mock(
(fn: () => void, ms: number) =>
originalSetTimeout(fn, ms) as unknown as NodeJS.Timeout,
);
global.setTimeout = setTimeoutMock as any;

try {
const requestId = "req-1";
const operationId = randomUUID();

manager.register(requestId, operationId, 5000);

expect(setTimeoutMock).toHaveBeenCalled();
} finally {
global.setTimeout = originalSetTimeout;
}
});

test("cancel() sends notifications/cancelled notification", async () => {
const requestId = "req-2";
const operationId = randomUUID();

manager.register(requestId, operationId, 30000);
await manager.cancel(operationId, "User requested cancellation");

expect(mockClient.notification).toHaveBeenCalledWith(
expect.objectContaining({
method: "notifications/cancelled",
params: expect.objectContaining({
requestId: requestId,
reason: "User requested cancellation",
}),
}),
);
});

test("cancel() updates operation status to cancelled", async () => {
const requestId = "req-3";
const operationId = randomUUID();

manager.register(requestId, operationId, 30000);
await manager.cancel(operationId);

// Verification would require access to the operation store
// The implementation should update the store's operation status
// This test verifies the method doesn't throw
});

test("cancel() clears timeout timer", async () => {
const originalClearTimeout = global.clearTimeout;
const clearTimeoutMock = mock(() => { });
global.clearTimeout = clearTimeoutMock as any;

try {
const requestId = "req-4";
const operationId = randomUUID();

manager.register(requestId, operationId, 30000);
await manager.cancel(operationId);

expect(clearTimeoutMock).toHaveBeenCalled();
} finally {
global.clearTimeout = originalClearTimeout;
}
});

test("onResponse() clears pending request", () => {
const requestId = "req-5";
const operationId = randomUUID();

manager.register(requestId, operationId, 30000);
manager.onResponse(requestId);

// Calling cancel after onResponse should not send notification
// because the request is no longer pending
});

test("onResponse() ignores unknown requestId", () => {
// Should not throw for unknown requestId
expect(() => manager.onResponse("unknown-id")).not.toThrow();
});

test("timeout auto-cancels operation", async () => {
// Use fake timers or short timeout
const requestId = "req-6";
const operationId = randomUUID();

// Register with very short timeout
manager.register(requestId, operationId, 50);

// Wait for timeout to fire
await new Promise((resolve) => setTimeout(resolve, 100));

// The implementation should have auto-cancelled
// Verify via notification call or store state
// For now, we verify that the timeout mechanism is wired up
});
});
Loading
Loading