Project 14: “The File System Guardian (Node.js)” — MCP Protocol Implementation
| Attribute | Value |
|---|---|
| File | KIRO_CLI_LEARNING_PROJECTS.md |
| Main Programming Language | TypeScript / Node.js |
| Coolness Level | Level 4: Hardcore Tech Flex |
| Difficulty | Level 3: Advanced |
| Knowledge Area | MCP Protocol Implementation |
What you’ll build: A sandboxed MCP server with high-level tools like scaffold_react_component(name, props).
Why it teaches Abstraction: You encode best practices into single, safe tools.
Success criteria:
- The tool generates consistent component structure every run.
Real World Outcome
You’ll have a TypeScript MCP server that provides high-level file system abstractions. Instead of letting the LLM call raw write_file repeatedly, you expose semantic tools like scaffold_react_component that encode your team’s best practices.
1. MCP Server Configuration (in ~/.config/kiro/settings.json):
{
"mcpServers": {
"fs-guardian": {
"command": "node",
"args": ["/path/to/fs-guardian/dist/index.js"],
"env": {
"WORKSPACE_ROOT": "/path/to/your/project",
"ALLOWED_DIRS": "src/components,src/lib,tests"
}
}
}
}
2. Kiro CLI Session:
$ kiro
You: Create a Button component that accepts label, onClick, and variant (primary/secondary) props
[Tool Call] fs-guardian.scaffold_react_component({
name: "Button",
props: ["label: string", "onClick: () => void", "variant: 'primary' | 'secondary'"]
})
Tool Response:
{
"success": true,
"files_created": [
"src/components/Button/Button.tsx",
"src/components/Button/Button.test.tsx",
"src/components/Button/Button.module.css",
"src/components/Button/index.ts"
],
"message": "React component 'Button' scaffolded successfully"
}
Kiro: I've created the Button component with the requested props. The component includes:
- TypeScript definition with proper prop types
- Jest unit test scaffolding
- CSS module for styling
- Barrel export in index.ts
All files are in src/components/Button/
3. Generated Files:
src/components/Button/Button.tsx:
import React from 'react';
import styles from './Button.module.css';
interface ButtonProps {
label: string;
onClick: () => void;
variant: 'primary' | 'secondary';
}
export const Button: React.FC<ButtonProps> = ({ label, onClick, variant }) => {
return (
<button
className={`${styles.button} ${styles[variant]}`}
onClick={onClick}
>
{label}
</button>
);
};
src/components/Button/Button.test.tsx:
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders with label', () => {
render(<Button label="Click me" onClick={() => {}} variant="primary" />);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button label="Click" onClick={handleClick} variant="primary" />);
fireEvent.click(screen.getByText('Click'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
4. Server Logs:
[2025-01-02 15:45:00] INFO: FS Guardian MCP Server started
[2025-01-02 15:45:00] INFO: Workspace root: /path/to/your/project
[2025-01-02 15:45:00] INFO: Allowed directories: src/components, src/lib, tests
[2025-01-02 15:45:15] INFO: Tool called: scaffold_react_component
[2025-01-02 15:45:15] INFO: Validating path: src/components/Button
[2025-01-02 15:45:15] INFO: Creating directory: src/components/Button
[2025-01-02 15:45:15] INFO: Writing file: Button.tsx (142 lines)
[2025-01-02 15:45:15] INFO: Writing file: Button.test.tsx (87 lines)
[2025-01-02 15:45:15] INFO: Writing file: Button.module.css (24 lines)
[2025-01-02 15:45:15] INFO: Writing file: index.ts (1 line)
[2025-01-02 15:45:15] INFO: Success: 4 files created
What you’re seeing:
- Path sandboxing - Server refuses to write outside
ALLOWED_DIRS - Template generation - Consistent structure (component, test, styles, barrel export)
- Type safety - TypeScript interfaces generated from prop descriptions
- Best practices encoded - Testing setup, CSS modules, proper exports
- Atomic operations - All files created together or none at all
This pattern prevents the LLM from creating inconsistent file structures or writing to dangerous locations.
The Core Question You’re Answering
“How do I create safe, high-level abstractions over file system operations that encode organizational best practices?”
Think about the problem: If you give an LLM direct file system access via basic write_file tools, it might:
- Create components in random directories
- Forget to add tests
- Use inconsistent naming conventions
- Write to system directories (
/etc,/usr/bin) - Overwrite critical files
This project teaches you to:
- Build guardrails - Restrict operations to safe paths
- Encode patterns - Capture “how we do things here” in reusable tools
- Validate inputs - Ensure the LLM provides well-formed requests
- Provide feedback - Return structured results the LLM can reason about
- Make tools atomic - Either all files are created or none
By the end, you’ll understand how to transform low-level primitives into high-level, safe, team-specific abstractions.
Concepts You Must Understand First
Stop and research these before coding:
- Path Traversal Attacks
- What is a path traversal attack? (e.g.,
../../etc/passwd) - How do you validate that a path is within allowed directories?
- What’s the difference between relative and absolute paths in validation?
- How does
path.resolve()help prevent directory traversal? - Book Reference: “The Web Application Hacker’s Handbook” by Stuttard & Pinto - Ch. 10
- What is a path traversal attack? (e.g.,
- File System Atomicity
- What happens if your script crashes halfway through creating files?
- How do you ensure “all or nothing” behavior?
- What’s a transaction-like pattern for file operations?
- How would you implement rollback if one file write fails?
- Book Reference: “Operating Systems: Three Easy Pieces” - Ch. 40 (File System Implementation)
- Template Systems
- How do you generate code from templates without embedding business logic in strings?
- What’s the difference between string interpolation and proper templating?
- How do you ensure generated code is syntactically valid?
- Should you use a library (Handlebars, EJS) or custom logic?
- Book Reference: “Compilers: Principles and Practice” - Ch. 2 (Lexical Analysis)
- TypeScript MCP Server Structure
- How do you type MCP requests and responses in TypeScript?
- What’s the recommended way to handle stdio in Node.js? (
readline, streams) - How do you structure a TypeScript project for deployment?
- What build process converts TS to JS for distribution?
- Book Reference: “Programming TypeScript” by Boris Cherny - Ch. 10 (Modules)
Questions to Guide Your Design
Before implementing, think through these:
- Safety Boundaries
- Which directories should be allowed? (Only
src/? Or alsotests/,docs/?) - Should you allow overwriting existing files, or only create new ones?
- How will you communicate “permission denied” errors to the LLM clearly?
- Should you log all file operations for audit purposes?
- Which directories should be allowed? (Only
- Tool Granularity
- Should you create one tool per component type? (
scaffold_react_component,scaffold_vue_component) - Or one generic
scaffold_componentthat takesframeworkas a parameter? - How many templates do you need to support your team’s patterns?
- Should tests be optional or always included?
- Should you create one tool per component type? (
- Validation
- How will you parse the
propsparameter? (Array of strings? TypeScript syntax?) - What if the LLM provides invalid TypeScript type syntax?
- Should you validate component names against naming conventions? (PascalCase? No special chars?)
- How will you handle edge cases like empty prop lists?
- How will you parse the
- Error Reporting
- If a file already exists, return an error or auto-increment the name?
- If path validation fails, explain why in a way the LLM can fix?
- Should partial failures (3/4 files written) trigger a rollback?
- How much detail should error messages include?
Thinking Exercise
Design the Sandboxing Logic
Before coding, trace what happens when the LLM tries to exploit your server:
Attack 1: Path Traversal
scaffold_react_component({
name: "../../../../../../etc/SystemConfig"
})
Your validation logic:
- Resolve
src/components/../../../../../../etc/SystemConfigto absolute path - Check if resolved path starts with
WORKSPACE_ROOT + "/src/components" - If not, reject with error: “Path outside allowed directories”
Attack 2: Overwrite Critical Files
scaffold_react_component({
name: "../../../package.json"
})
Your validation logic:
- Resolve to
/path/to/project/package.json - Check against allowed directories
- Reject (even though
package.jsonexists, it’s not insrc/components)
Attack 3: Malformed Props
scaffold_react_component({
name: "Button",
props: ["onClick: () => { console.log('pwned'); return void; }"]
})
Your prop parser:
- Split
onClick: () => { console.log('pwned'); return void; }on: - Extract type:
() => { console.log('pwned'); return void; } - Validate it’s a valid TypeScript type (complex function types are allowed)
- Generate interface:
onClick: () => { console.log('pwned'); return void; }
Questions while designing:
- Should you sanitize prop types, or trust the LLM to provide valid TypeScript?
- What if the prop type includes backticks or quotes that break template strings?
- How do you detect if a path is absolute vs. relative?
- Should you allow symlinks, or only real directories?
The Interview Questions They’ll Ask
Prepare to answer these:
-
“Explain how path traversal attacks work. How does your MCP server prevent
../../etc/passwdfrom being written?” -
“Your server creates 4 files per component. What happens if the 3rd file write fails? How do you ensure atomicity?”
-
“How would you extend this server to support different component frameworks (React, Vue, Svelte) without duplicating code?”
-
“If the LLM provides a prop type like
data: Array<{id: number, nested: {value: string}}>, how do you parse and validate it?” -
“What’s the security difference between validating paths before resolution vs. after
path.resolve()?” -
“How would you implement rate limiting to prevent the LLM from creating 1000 components in a loop?”
-
“Describe how you’d test this MCP server. Can you unit test it without running Kiro?”
Hints in Layers
Hint 1: Starting Point
Start by implementing path validation. Create a validatePath(targetPath) function that:
- Resolves the path to absolute
- Checks if it starts with one of
ALLOWED_DIRS - Returns
{valid: true}or{valid: false, reason: "..."}
Test this with various malicious inputs before building the rest.
Hint 2: Atomicity Pattern Collect all file writes in an array, then execute them all at once:
const operations = [
{ path: 'Button.tsx', content: '...' },
{ path: 'Button.test.tsx', content: '...' },
{ path: 'Button.module.css', content: '...' },
{ path: 'index.ts', content: '...' }
];
try {
for (const op of operations) {
await fs.writeFile(op.path, op.content);
}
} catch (error) {
// Rollback: delete all files created so far
for (const op of operations) {
await fs.unlink(op.path).catch(() => {});
}
throw error;
}
Hint 3: Template Generation Use template literals with a helper function:
function generateComponent(name: string, props: Array<{name: string, type: string}>) {
const propsInterface = props.map(p => ` ${p.name}: ${p.type};`).join('\n');
const destructuredProps = props.map(p => p.name).join(', ');
return `import React from 'react';
import styles from './${name}.module.css';
interface ${name}Props {
${propsInterface}
}
export const ${name}: React.FC<${name}Props> = ({ ${destructuredProps} }) => {
return <div className={styles.${name.toLowerCase()}}>{/* TODO */}</div>;
};`;
}
Hint 4: Prop Parsing
Parse prop strings like "label: string" using regex:
function parseProp(propString: string): {name: string, type: string} {
const match = propString.match(/^(\w+):\s*(.+)$/);
if (!match) throw new Error(`Invalid prop: ${propString}`);
return { name: match[1], type: match[2].trim() };
}
Hint 5: Path Validation
Use path.resolve() and startsWith():
import path from 'path';
function isPathAllowed(targetPath: string): boolean {
const resolved = path.resolve(process.env.WORKSPACE_ROOT!, targetPath);
const allowedDirs = process.env.ALLOWED_DIRS!.split(',');
return allowedDirs.some(dir => {
const allowedPath = path.resolve(process.env.WORKSPACE_ROOT!, dir);
return resolved.startsWith(allowedPath);
});
}
Hint 6: MCP Protocol Handling Reuse the JSON-RPC handler pattern from Project 13:
async function handleToolCall(params: any) {
const { name, arguments: args } = params;
if (name === 'scaffold_react_component') {
return scaffoldReactComponent(args.name, args.props);
}
throw new Error(`Unknown tool: ${name}`);
}
Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Path Traversal & Security | “The Web Application Hacker’s Handbook” by Stuttard | Ch. 10 (Attacking Back-End Components) |
| File System Operations | “Operating Systems: Three Easy Pieces” | Ch. 39-40 (Files and Directories) |
| Template Generation | “Compilers: Principles and Practice” | Ch. 2 (Lexical Analysis) |
| TypeScript Best Practices | “Programming TypeScript” by Boris Cherny | Ch. 10 (Modules and Namespaces) |
| Node.js Streams | “Node.js Design Patterns” by Mario Casciaro | Ch. 5 (Streams) |
Common Pitfalls & Debugging
Problem 1: “Path validation allows ../ escapes”
- Why: You’re validating the path string instead of the resolved absolute path
- Fix: Always use
path.resolve()before validation:const absolutePath = path.resolve(workspaceRoot, userInput); if (!absolutePath.startsWith(workspaceRoot)) { throw new Error("Path outside workspace"); } - Quick test: Try
validatePath("src/../../etc/passwd")and ensure it’s rejected
Problem 2: “File write succeeds but file is empty”
- Why: You forgot to
awaitthe write operation, or the content variable is undefined - Fix: Always
await fs.writeFile()and log the content length:console.error(`[DEBUG] Writing ${content.length} bytes to ${filePath}`); await fs.writeFile(filePath, content, 'utf-8'); - Quick test: Check file size after write:
ls -lh src/components/Button/Button.tsx
Problem 3: “TypeScript build fails with Cannot find module“
- Why: Your
tsconfig.jsondoesn’t include the MCP server files - Fix: Update
includeto cover all source files:{ "include": ["src/**/*", "index.ts"], "compilerOptions": { "outDir": "./dist", "rootDir": "./" } } - Quick test: Run
tsc --noEmitto check for errors without building
Problem 4: “Server creates files but Kiro sees ‘Tool call failed’“
- Why: You’re not returning a valid MCP response (missing
resultor wrong structure) - Fix: Ensure you return
{success: true, files_created: [...]}:return { content: [{ type: "text", text: JSON.stringify({ success: true, files_created: filePaths, message: `Component '${name}' scaffolded successfully` }) }] }; - Quick test: Check server logs for the exact JSON response sent
Problem 5: “Rollback doesn’t work—partial files remain”
- Why: Unlink errors are swallowed silently, or files aren’t tracked properly
- Fix: Log rollback operations:
for (const filePath of createdFiles) { try { await fs.unlink(filePath); console.error(`[ROLLBACK] Deleted ${filePath}`); } catch (err) { console.error(`[ROLLBACK FAILED] ${filePath}: ${err}`); } } - Quick test: Simulate a write failure on the 3rd file and verify all previous files are deleted
Definition of Done
- Path validation correctly rejects
../traversal attempts - Server refuses to write outside
ALLOWED_DIRS(test with/etc/passwd) scaffold_react_componentgenerates all 4 files (component, test, CSS, barrel export)- Generated TypeScript compiles without errors (
tsc --noEmit) - Generated tests run successfully (
npm test) - Partial failures trigger rollback (all files deleted if any write fails)
- Server logs all file operations to stderr for audit trail
- Invalid component names return clear errors (e.g.,
"invalid-name"with hyphens) - Prop parsing handles complex types (e.g.,
Array<{id: number}>) - README.md documents the tool schema and example Kiro settings configuration