Skip to content

Commit f4d8222

Browse files
Merge pull request #34 from chriscarrollsmith/13-cli-command-to-generate-a-project-plan-with-a-structured-output-llm-api-call-to-a-smart-model
Tool to generate a project plan with an LLM
2 parents a75f60e + b8e6949 commit f4d8222

29 files changed

+3405
-929
lines changed

.cursor/rules/MCP_clients.mdc

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
description:
3+
globs: tests/integration/mcp-client.test.ts
4+
alwaysApply: false
5+
---
6+
### Writing MCP Clients
7+
8+
The SDK provides a high-level client interface:
9+
10+
```typescript
11+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
12+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
13+
14+
const transport = new StdioClientTransport({
15+
command: "node",
16+
args: ["server.js"]
17+
});
18+
19+
const client = new Client(
20+
{
21+
name: "example-client",
22+
version: "1.0.0"
23+
},
24+
{
25+
capabilities: {
26+
prompts: {},
27+
resources: {},
28+
tools: {}
29+
}
30+
}
31+
);
32+
33+
await client.connect(transport);
34+
35+
// List prompts
36+
const prompts = await client.listPrompts();
37+
38+
// Get a prompt
39+
const prompt = await client.getPrompt("example-prompt", {
40+
arg1: "value"
41+
});
42+
43+
// List resources
44+
const resources = await client.listResources();
45+
46+
// Read a resource
47+
const resource = await client.readResource("file:///example.txt");
48+
49+
// Call a tool
50+
const result = await client.callTool({
51+
name: "example-tool",
52+
arguments: {
53+
arg1: "value"
54+
}
55+
});
56+
```

.cursor/rules/MCP_implementation.mdc

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
description:
3+
globs: index.ts
4+
alwaysApply: false
5+
---
6+
# MCP TypeScript SDK
7+
8+
## What is MCP?
9+
10+
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:
11+
12+
- Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context)
13+
- Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect)
14+
- Define interaction patterns through **Prompts** (reusable templates for LLM interactions)
15+
16+
## Running Your Server
17+
18+
MCP servers in TypeScript need to be connected to a transport to communicate with clients.
19+
20+
```typescript
21+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
22+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23+
import {
24+
ListPromptsRequestSchema,
25+
GetPromptRequestSchema
26+
} from "@modelcontextprotocol/sdk/types.js";
27+
28+
const server = new Server(
29+
{
30+
name: "example-server",
31+
version: "1.0.0"
32+
},
33+
{
34+
capabilities: {
35+
prompts: {}
36+
}
37+
}
38+
);
39+
40+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
41+
return {
42+
prompts: [{
43+
name: "example-prompt",
44+
description: "An example prompt template",
45+
arguments: [{
46+
name: "arg1",
47+
description: "Example argument",
48+
required: true
49+
}]
50+
}]
51+
};
52+
});
53+
54+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
55+
if (request.params.name !== "example-prompt") {
56+
throw new Error("Unknown prompt");
57+
}
58+
return {
59+
description: "Example prompt",
60+
messages: [{
61+
role: "user",
62+
content: {
63+
type: "text",
64+
text: "Example prompt text"
65+
}
66+
}]
67+
};
68+
});
69+
70+
const transport = new StdioServerTransport();
71+
await server.connect(transport);
72+
```

.cursor/rules/MCP_remote.mdc

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
description:
3+
globs:
4+
alwaysApply: false
5+
---
6+
### HTTP with SSE
7+
8+
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:
9+
10+
```typescript
11+
import express from "express";
12+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
14+
15+
const server = new McpServer({
16+
name: "example-server",
17+
version: "1.0.0"
18+
});
19+
20+
// ... set up server resources, tools, and prompts ...
21+
22+
const app = express();
23+
24+
app.get("/sse", async (req, res) => {
25+
const transport = new SSEServerTransport("/messages", res);
26+
await server.connect(transport);
27+
});
28+
29+
app.post("/messages", async (req, res) => {
30+
// Note: to support multiple simultaneous connections, these messages will
31+
// need to be routed to a specific matching transport. (This logic isn't
32+
// implemented here, for simplicity.)
33+
await transport.handlePostMessage(req, res);
34+
});
35+
36+
app.listen(3001);
37+
```

.cursor/rules/cli-tests.mdc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
description:
3+
globs: tests/integration/cli.test.ts
4+
alwaysApply: false
5+
---
6+
**CLI Testing**:
7+
- When testing CLI commands, pass the environment variable inline:
8+
```typescript
9+
const { stdout } = await execAsync(
10+
`TASK_MANAGER_FILE_PATH=${tasksFilePath} tsx ${CLI_PATH} command`
11+
);
12+
```
13+
- Use `tsx` instead of `node` for running TypeScript files directly

.cursor/rules/tests.mdc

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
---
2+
description: Writing unit tests with `jest`
3+
globs: tests/**/*
4+
alwaysApply: false
5+
---
6+
# Testing Guidelines for TypeScript + ES Modules + Jest
7+
8+
This guide contains cumulative in-context learnings about working with this project's testing stack.
9+
10+
## Unit vs. Integration Tests
11+
12+
**Never Mix Test Types**: Separate integration tests from unit tests into different files:
13+
- Simple unit tests without mocks for validating rules (like state transitions)
14+
- Integration tests with mocks for filesystem and external dependencies
15+
16+
## File Path Handling in Tests
17+
18+
1. **Environment Variables**:
19+
- Use `process.env.TASK_MANAGER_FILE_PATH` for configuring file paths in tests
20+
- Set this in `beforeEach` and clean up in `afterEach`:
21+
```typescript
22+
beforeEach(async () => {
23+
tempDir = path.join(os.tmpdir(), `test-${Date.now()}`);
24+
await fs.mkdir(tempDir, { recursive: true });
25+
tasksFilePath = path.join(tempDir, "test-tasks.json");
26+
process.env.TASK_MANAGER_FILE_PATH = tasksFilePath;
27+
});
28+
29+
afterEach(async () => {
30+
await fs.rm(tempDir, { recursive: true, force: true });
31+
delete process.env.TASK_MANAGER_FILE_PATH;
32+
});
33+
```
34+
35+
2. **Temporary Files**:
36+
- Create unique temp directories for each test run
37+
- Use `os.tmpdir()` for platform-independent temp directories
38+
- Include timestamps in directory names to prevent conflicts
39+
- Always clean up temp files in `afterEach`
40+
41+
## Jest ESM Mocking, Step-by-Step
42+
43+
1. **Type-Only Import:**
44+
Import types for static analysis without actually executing the module code:
45+
```typescript
46+
import type { MyService as MyServiceType } from 'path/to/MyService.js';
47+
import type { readFile as ReadFileType } from 'node:fs/promises';
48+
```
49+
50+
2. **Register Mock:**
51+
Use `jest.unstable_mockModule` to replace the real module:
52+
```typescript
53+
jest.unstable_mockModule('node:fs/promises', () => ({
54+
__esModule: true,
55+
readFile: jest.fn(),
56+
}));
57+
```
58+
59+
3. **Set Default Mock Implementations, Then Dynamically Import Modules:**
60+
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.
61+
```typescript
62+
let MyService: typeof MyServiceType;
63+
let readFile: jest.MockedFunction<ReadFileType>;
64+
65+
beforeAll(async () => {
66+
const fsPromisesMock = await import('node:fs/promises');
67+
readFile = fsPromisesMock.readFile as jest.MockedFunction<ReadFileType>;
68+
69+
// Set default implementation
70+
readFile.mockResolvedValue('default mocked content');
71+
72+
const serviceModule = await import('path/to/MyService.js');
73+
MyService = serviceModule.MyService;
74+
});
75+
```
76+
77+
4. **Setup in `beforeEach`:**
78+
Reset mocks and set default behaviors before each test:
79+
```typescript
80+
beforeEach(() => {
81+
jest.clearAllMocks();
82+
readFile.mockResolvedValue('');
83+
});
84+
```
85+
86+
5. **Write a Test:**
87+
Now you can test your service with the mocked `readFile`:
88+
```typescript
89+
describe('MyService', () => {
90+
let myServiceInstance: MyServiceType;
91+
92+
beforeEach(() => {
93+
myServiceInstance = new MyService('somePath');
94+
});
95+
96+
it('should do something', async () => {
97+
readFile.mockResolvedValueOnce('some data');
98+
const result = await myServiceInstance.someMethod();
99+
expect(result).toBe('expected result');
100+
expect(readFile).toHaveBeenCalledWith('somePath', 'utf-8');
101+
});
102+
});
103+
```
104+
105+
### Mocking a Class with Methods
106+
107+
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:
108+
109+
```typescript
110+
// 1. Create typed jest mock functions using the original types
111+
type InitResult = { data: string };
112+
113+
const mockInit = jest.fn() as jest.MockedFunction<MyClass['init']>;
114+
const mockDoWork = jest.fn() as jest.MockedFunction<MyClass['doWork']>;
115+
const mockStaticHelper = jest.fn() as jest.MockedFunction<typeof MyClass.staticHelper>;
116+
117+
// 2. Use jest.unstable_mockModule with an ES6 class in the factory
118+
jest.unstable_mockModule('path/to/MyClass.js', () => {
119+
class MockMyClass {
120+
// Instance methods
121+
init = mockInit;
122+
doWork = mockDoWork;
123+
124+
// Static method
125+
static staticHelper = mockStaticHelper;
126+
}
127+
128+
return {
129+
__esModule: true,
130+
MyClass: MockMyClass, // same name/structure as real export
131+
};
132+
});
133+
134+
// 3. Import your class after mocking
135+
let MyClass: typeof import('path/to/MyClass.js')['MyClass'];
136+
137+
beforeAll(async () => {
138+
const myClassModule = await import('path/to/MyClass.js');
139+
MyClass = myClassModule.MyClass;
140+
});
141+
142+
// 4. Write tests and reset mocks
143+
beforeEach(() => {
144+
jest.clearAllMocks();
145+
mockInit.mockResolvedValue({ data: 'default' });
146+
mockStaticHelper.mockReturnValue(42);
147+
});
148+
149+
describe('MyClass', () => {
150+
it('should call init', async () => {
151+
const instance = new MyClass();
152+
const result = await instance.init();
153+
expect(result).toEqual({ data: 'default' });
154+
expect(mockInit).toHaveBeenCalledTimes(1);
155+
});
156+
157+
it('should call the static helper', () => {
158+
const val = MyClass.staticHelper();
159+
expect(val).toBe(42);
160+
expect(mockStaticHelper).toHaveBeenCalledTimes(1);
161+
});
162+
});
163+
```
164+
165+
### Best Practice: **Type** Your Mocked Functions
166+
167+
By default, `jest.fn()` is very generic and doesn't enforce parameter or return types. This can cause TypeScript errors like:
168+
169+
> `Argument of type 'undefined' is not assignable to parameter of type 'never'`
170+
171+
or
172+
173+
> `Type 'Promise<SomeType>' is not assignable to type 'FunctionLike'`
174+
175+
To avoid these, **use the original type with `jest.MockedFunction`**. For example, if your real function is:
176+
177+
```typescript
178+
async function loadStuff(id: string): Promise<string[]> {
179+
// ...
180+
}
181+
```
182+
183+
then you should type the mock as:
184+
185+
```typescript
186+
const mockLoadStuff = jest.fn() as jest.MockedFunction<typeof loadStuff>;
187+
```
188+
189+
For class methods, use the class type to get the method signature:
190+
191+
```typescript
192+
const mockClassMethod = jest.fn() as jest.MockedFunction<YourClass['classMethod']>;
193+
```
194+
195+
This helps TypeScript catch mistakes if you:
196+
- call the function with the wrong argument types
197+
- use `mockResolvedValue` with the wrong shape
198+
199+
Once typed properly, your `mockResolvedValue(...)`, `mockImplementation(...)`, etc. calls will be fully type-safe.

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ node_modules
66
.vscode
77
.env
88
.env.local
9-
.cursor
109
artifacts
1110
repomix-output.txt
1211

0 commit comments

Comments
 (0)