Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
56 changes: 56 additions & 0 deletions .cursor/rules/MCP_clients.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
description:
globs: tests/integration/mcp-client.test.ts
alwaysApply: false
---
### Writing MCP Clients

The SDK provides a high-level client interface:

```typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const transport = new StdioClientTransport({
command: "node",
args: ["server.js"]
});

const client = new Client(
{
name: "example-client",
version: "1.0.0"
},
{
capabilities: {
prompts: {},
resources: {},
tools: {}
}
}
);

await client.connect(transport);

// List prompts
const prompts = await client.listPrompts();

// Get a prompt
const prompt = await client.getPrompt("example-prompt", {
arg1: "value"
});

// List resources
const resources = await client.listResources();

// Read a resource
const resource = await client.readResource("file:///example.txt");

// Call a tool
const result = await client.callTool({
name: "example-tool",
arguments: {
arg1: "value"
}
});
```
72 changes: 72 additions & 0 deletions .cursor/rules/MCP_implementation.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
description:
globs: index.ts
alwaysApply: false
---
# MCP TypeScript SDK

## What is MCP?

The Model Context Protocol lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can:

- Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context)
- Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect)
- Define interaction patterns through **Prompts** (reusable templates for LLM interactions)

## Running Your Server

MCP servers in TypeScript need to be connected to a transport to communicate with clients.

```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
ListPromptsRequestSchema,
GetPromptRequestSchema
} from "@modelcontextprotocol/sdk/types.js";

const server = new Server(
{
name: "example-server",
version: "1.0.0"
},
{
capabilities: {
prompts: {}
}
}
);

server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [{
name: "example-prompt",
description: "An example prompt template",
arguments: [{
name: "arg1",
description: "Example argument",
required: true
}]
}]
};
});

server.setRequestHandler(GetPromptRequestSchema, async (request) => {
if (request.params.name !== "example-prompt") {
throw new Error("Unknown prompt");
}
return {
description: "Example prompt",
messages: [{
role: "user",
content: {
type: "text",
text: "Example prompt text"
}
}]
};
});

const transport = new StdioServerTransport();
await server.connect(transport);
```
37 changes: 37 additions & 0 deletions .cursor/rules/MCP_remote.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
description:
globs:
alwaysApply: false
---
### HTTP with SSE

For remote servers, start a web server with a Server-Sent Events (SSE) endpoint, and a separate endpoint for the client to send its messages to:

```typescript
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";

const server = new McpServer({
name: "example-server",
version: "1.0.0"
});

// ... set up server resources, tools, and prompts ...

const app = express();

app.get("/sse", async (req, res) => {
const transport = new SSEServerTransport("/messages", res);
await server.connect(transport);
});

app.post("/messages", async (req, res) => {
// Note: to support multiple simultaneous connections, these messages will
// need to be routed to a specific matching transport. (This logic isn't
// implemented here, for simplicity.)
await transport.handlePostMessage(req, res);
});

app.listen(3001);
```
13 changes: 13 additions & 0 deletions .cursor/rules/cli-tests.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
description:
globs: tests/integration/cli.test.ts
alwaysApply: false
---
**CLI Testing**:
- When testing CLI commands, pass the environment variable inline:
```typescript
const { stdout } = await execAsync(
`TASK_MANAGER_FILE_PATH=${tasksFilePath} tsx ${CLI_PATH} command`
);
```
- Use `tsx` instead of `node` for running TypeScript files directly
199 changes: 199 additions & 0 deletions .cursor/rules/tests.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
---
description: Writing unit tests with `jest`
globs: tests/**/*
alwaysApply: false
---
# Testing Guidelines for TypeScript + ES Modules + Jest

This guide contains cumulative in-context learnings about working with this project's testing stack.

## Unit vs. Integration Tests

**Never Mix Test Types**: Separate integration tests from unit tests into different files:
- Simple unit tests without mocks for validating rules (like state transitions)
- Integration tests with mocks for filesystem and external dependencies

## File Path Handling in Tests

1. **Environment Variables**:
- Use `process.env.TASK_MANAGER_FILE_PATH` for configuring file paths in tests
- Set this in `beforeEach` and clean up in `afterEach`:
```typescript
beforeEach(async () => {
tempDir = path.join(os.tmpdir(), `test-${Date.now()}`);
await fs.mkdir(tempDir, { recursive: true });
tasksFilePath = path.join(tempDir, "test-tasks.json");
process.env.TASK_MANAGER_FILE_PATH = tasksFilePath;
});

afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
delete process.env.TASK_MANAGER_FILE_PATH;
});
```

2. **Temporary Files**:
- Create unique temp directories for each test run
- Use `os.tmpdir()` for platform-independent temp directories
- Include timestamps in directory names to prevent conflicts
- Always clean up temp files in `afterEach`

## Jest ESM Mocking, Step-by-Step

1. **Type-Only Import:**
Import types for static analysis without actually executing the module code:
```typescript
import type { MyService as MyServiceType } from 'path/to/MyService.js';
import type { readFile as ReadFileType } from 'node:fs/promises';
```

2. **Register Mock:**
Use `jest.unstable_mockModule` to replace the real module:
```typescript
jest.unstable_mockModule('node:fs/promises', () => ({
__esModule: true,
readFile: jest.fn(),
}));
```

3. **Set Default Mock Implementations, Then Dynamically Import Modules:**
You must dynamically import the modules to be mocked and/or tested *after* registering mocks and setting any mock implementations. This ensures that when `MyService` attempts to import `node:fs/promises`, it gets your mocked version. Depending how you want to scope your mock implementations, you can do this in `beforeAll`, `beforeEach`, or at the top of each test.
```typescript
let MyService: typeof MyServiceType;
let readFile: jest.MockedFunction<ReadFileType>;

beforeAll(async () => {
const fsPromisesMock = await import('node:fs/promises');
readFile = fsPromisesMock.readFile as jest.MockedFunction<ReadFileType>;

// Set default implementation
readFile.mockResolvedValue('default mocked content');

const serviceModule = await import('path/to/MyService.js');
MyService = serviceModule.MyService;
});
```

4. **Setup in `beforeEach`:**
Reset mocks and set default behaviors before each test:
```typescript
beforeEach(() => {
jest.clearAllMocks();
readFile.mockResolvedValue('');
});
```

5. **Write a Test:**
Now you can test your service with the mocked `readFile`:
```typescript
describe('MyService', () => {
let myServiceInstance: MyServiceType;

beforeEach(() => {
myServiceInstance = new MyService('somePath');
});

it('should do something', async () => {
readFile.mockResolvedValueOnce('some data');
const result = await myServiceInstance.someMethod();
expect(result).toBe('expected result');
expect(readFile).toHaveBeenCalledWith('somePath', 'utf-8');
});
});
```

### Mocking a Class with Methods

If you have a class `MyClass` that has both instance methods and static methods, you can mock it in an **ES Modules + TypeScript** setup using the same pattern. For instance:

```typescript
// 1. Create typed jest mock functions using the original types
type InitResult = { data: string };

const mockInit = jest.fn() as jest.MockedFunction<MyClass['init']>;
const mockDoWork = jest.fn() as jest.MockedFunction<MyClass['doWork']>;
const mockStaticHelper = jest.fn() as jest.MockedFunction<typeof MyClass.staticHelper>;

// 2. Use jest.unstable_mockModule with an ES6 class in the factory
jest.unstable_mockModule('path/to/MyClass.js', () => {
class MockMyClass {
// Instance methods
init = mockInit;
doWork = mockDoWork;

// Static method
static staticHelper = mockStaticHelper;
}

return {
__esModule: true,
MyClass: MockMyClass, // same name/structure as real export
};
});

// 3. Import your class after mocking
let MyClass: typeof import('path/to/MyClass.js')['MyClass'];

beforeAll(async () => {
const myClassModule = await import('path/to/MyClass.js');
MyClass = myClassModule.MyClass;
});

// 4. Write tests and reset mocks
beforeEach(() => {
jest.clearAllMocks();
mockInit.mockResolvedValue({ data: 'default' });
mockStaticHelper.mockReturnValue(42);
});

describe('MyClass', () => {
it('should call init', async () => {
const instance = new MyClass();
const result = await instance.init();
expect(result).toEqual({ data: 'default' });
expect(mockInit).toHaveBeenCalledTimes(1);
});

it('should call the static helper', () => {
const val = MyClass.staticHelper();
expect(val).toBe(42);
expect(mockStaticHelper).toHaveBeenCalledTimes(1);
});
});
```

### Best Practice: **Type** Your Mocked Functions

By default, `jest.fn()` is very generic and doesn't enforce parameter or return types. This can cause TypeScript errors like:

> `Argument of type 'undefined' is not assignable to parameter of type 'never'`

or

> `Type 'Promise<SomeType>' is not assignable to type 'FunctionLike'`

To avoid these, **use the original type with `jest.MockedFunction`**. For example, if your real function is:

```typescript
async function loadStuff(id: string): Promise<string[]> {
// ...
}
```

then you should type the mock as:

```typescript
const mockLoadStuff = jest.fn() as jest.MockedFunction<typeof loadStuff>;
```

For class methods, use the class type to get the method signature:

```typescript
const mockClassMethod = jest.fn() as jest.MockedFunction<YourClass['classMethod']>;
```

This helps TypeScript catch mistakes if you:
- call the function with the wrong argument types
- use `mockResolvedValue` with the wrong shape

Once typed properly, your `mockResolvedValue(...)`, `mockImplementation(...)`, etc. calls will be fully type-safe.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ node_modules
.vscode
.env
.env.local
.cursor
artifacts
repomix-output.txt

Expand Down
Loading