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:

  1. 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
  2. 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)
  3. 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)
  4. 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:

  1. Safety Boundaries
    • Which directories should be allowed? (Only src/? Or also tests/, 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?
  2. Tool Granularity
    • Should you create one tool per component type? (scaffold_react_component, scaffold_vue_component)
    • Or one generic scaffold_component that takes framework as a parameter?
    • How many templates do you need to support your team’s patterns?
    • Should tests be optional or always included?
  3. Validation
    • How will you parse the props parameter? (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?
  4. 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:

  1. Resolve src/components/../../../../../../etc/SystemConfig to absolute path
  2. Check if resolved path starts with WORKSPACE_ROOT + "/src/components"
  3. If not, reject with error: “Path outside allowed directories”

Attack 2: Overwrite Critical Files

scaffold_react_component({
  name: "../../../package.json"
})

Your validation logic:

  1. Resolve to /path/to/project/package.json
  2. Check against allowed directories
  3. Reject (even though package.json exists, it’s not in src/components)

Attack 3: Malformed Props

scaffold_react_component({
  name: "Button",
  props: ["onClick: () => { console.log('pwned'); return void; }"]
})

Your prop parser:

  1. Split onClick: () => { console.log('pwned'); return void; } on :
  2. Extract type: () => { console.log('pwned'); return void; }
  3. Validate it’s a valid TypeScript type (complex function types are allowed)
  4. 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:

  1. “Explain how path traversal attacks work. How does your MCP server prevent ../../etc/passwd from being written?”

  2. “Your server creates 4 files per component. What happens if the 3rd file write fails? How do you ensure atomicity?”

  3. “How would you extend this server to support different component frameworks (React, Vue, Svelte) without duplicating code?”

  4. “If the LLM provides a prop type like data: Array<{id: number, nested: {value: string}}>, how do you parse and validate it?”

  5. “What’s the security difference between validating paths before resolution vs. after path.resolve()?”

  6. “How would you implement rate limiting to prevent the LLM from creating 1000 components in a loop?”

  7. “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:

  1. Resolves the path to absolute
  2. Checks if it starts with one of ALLOWED_DIRS
  3. 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 await the 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.json doesn’t include the MCP server files
  • Fix: Update include to cover all source files:
    {
    "include": ["src/**/*", "index.ts"],
    "compilerOptions": {
      "outDir": "./dist",
      "rootDir": "./"
    }
    }
    
  • Quick test: Run tsc --noEmit to 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 result or 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_component generates 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