Skip to main content
Lesson 7 of 7 60 min

Capstone: Simple Loom

Build a Task Tracker MCP server. Apply everything you've learned.

Time to Build

You've learned the philosophy. Now you apply it.

You're building Simple Loom — a Task Tracker MCP server. When you're done, you'll be able to tell an AI agent "add a task" and have it actually saved to disk, persisting between sessions.

This is the automation layer pattern from Lesson 3, made real.


What You're Building

Your words                 Your MCP Server              What happens
──────────────────        ─────────────────            ──────────────
"Add a task"         →    Simple Loom         →       Task saved to disk
"What's on my list?" →    (your code)         →       Tasks returned
"Mark it done"       →                        →       Status updated

Four tools. One JSON file. That's the whole thing.


Checkpoint 1: Create the Project

What you're doing: Setting up a Node.js project with TypeScript and the MCP SDK.

mkdir ~/my-task-tracker
cd ~/my-task-tracker
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

Create the src directory:

mkdir src

Verify Checkpoint 1

Run ls in your project directory.

You should see:

node_modules/
src/
package.json
package-lock.json
tsconfig.json

If something's missing: Check that each command succeeded. Re-run any that failed.


Checkpoint 2: Create the Storage Layer

What you're doing: Building external memory — the code that persists tasks to disk.

Create src/tasks.ts:

import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';

export interface Task {
  id: string;
  title: string;
  status: 'todo' | 'doing' | 'done';
  createdAt: string;
}

const TASKS_DIR = path.join(os.homedir(), '.tasks');
const TASKS_FILE = path.join(TASKS_DIR, 'tasks.json');

function ensureDir() {
  if (!fs.existsSync(TASKS_DIR)) {
    fs.mkdirSync(TASKS_DIR, { recursive: true });
  }
}

export function loadTasks(): Task[] {
  ensureDir();
  if (!fs.existsSync(TASKS_FILE)) return [];
  return JSON.parse(fs.readFileSync(TASKS_FILE, 'utf-8'));
}

export function saveTasks(tasks: Task[]) {
  ensureDir();
  fs.writeFileSync(TASKS_FILE, JSON.stringify(tasks, null, 2));
}

export function addTask(title: string): Task {
  const tasks = loadTasks();
  const task: Task = {
    id: Date.now().toString(36),
    title,
    status: 'todo',
    createdAt: new Date().toISOString(),
  };
  tasks.push(task);
  saveTasks(tasks);
  return task;
}

export function getTasks(status?: Task['status']): Task[] {
  const tasks = loadTasks();
  return status ? tasks.filter(t => t.status === status) : tasks;
}

export function updateTaskStatus(id: string, status: Task['status']): Task | null {
  const tasks = loadTasks();
  const task = tasks.find(t => t.id === id);
  if (!task) return null;
  task.status = status;
  saveTasks(tasks);
  return task;
}

export function removeTask(id: string): boolean {
  const tasks = loadTasks();
  const index = tasks.findIndex(t => t.id === id);
  if (index === -1) return false;
  tasks.splice(index, 1);
  saveTasks(tasks);
  return true;
}

This is the external memory pattern from Lesson 5. Tasks live at ~/.tasks/tasks.json.

Verify Checkpoint 2

Try compiling:

npx tsc

You should see: No output (success means no errors).

If you get errors: Check for typos in src/tasks.ts. TypeScript errors usually point to the exact line and problem.


Checkpoint 3: Create the MCP Server

What you're doing: Building the server that exposes your tools to AI agents.

Create src/index.ts:

#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { addTask, getTasks, updateTaskStatus, removeTask, Task } from './tasks.js';

const server = new Server(
  { name: 'task-tracker', version: '1.0.0' },
  { capabilities: { tools: {} } }
);

// Define available tools (this is what agents read)
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: 'task_add',
      description: 'Add a new task to the task list',
      inputSchema: {
        type: 'object',
        properties: { 
          title: { type: 'string', description: 'The task title' } 
        },
        required: ['title'],
      },
    },
    {
      name: 'task_list',
      description: 'List all tasks, optionally filtered by status',
      inputSchema: {
        type: 'object',
        properties: {
          status: { 
            type: 'string', 
            enum: ['todo', 'doing', 'done'],
            description: 'Filter by status (optional)'
          },
        },
      },
    },
    {
      name: 'task_complete',
      description: 'Mark a task as done',
      inputSchema: {
        type: 'object',
        properties: { 
          id: { type: 'string', description: 'The task ID to complete' } 
        },
        required: ['id'],
      },
    },
    {
      name: 'task_remove',
      description: 'Remove a task permanently',
      inputSchema: {
        type: 'object',
        properties: { 
          id: { type: 'string', description: 'The task ID to remove' } 
        },
        required: ['id'],
      },
    },
  ],
}));

// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  switch (name) {
    case 'task_add': {
      const { title } = args as { title: string };
      const task = addTask(title);
      return { content: [{ type: 'text', text: JSON.stringify({ task }) }] };
    }

    case 'task_list': {
      const { status } = args as { status?: Task['status'] };
      const tasks = getTasks(status);
      return { content: [{ type: 'text', text: JSON.stringify({ tasks }) }] };
    }

    case 'task_complete': {
      const { id } = args as { id: string };
      const task = updateTaskStatus(id, 'done');
      if (!task) {
        return { content: [{ type: 'text', text: JSON.stringify({ error: 'Task not found', id }) }] };
      }
      return { content: [{ type: 'text', text: JSON.stringify({ task }) }] };
    }

    case 'task_remove': {
      const { id } = args as { id: string };
      const success = removeTask(id);
      return { content: [{ type: 'text', text: JSON.stringify({ success, id }) }] };
    }

    default:
      return { content: [{ type: 'text', text: JSON.stringify({ error: 'Unknown tool', name }) }] };
  }
});

// Start the server
const transport = new StdioServerTransport();
server.connect(transport);

Notice the patterns from Lesson 6:

  • Detailed schemas: Each property has a description
  • Structured output: Always returning JSON
  • Structured errors: Errors include context for recovery

Verify Checkpoint 3

Compile again:

npx tsc

You should see: No output (success).

If you get errors: Most likely an import path issue. Make sure you're importing from './tasks.js' (with .js extension — required for Node ESM).


Checkpoint 4: Test the Server Manually

What you're doing: Verifying the server responds to MCP requests.

Run this:

echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node dist/index.js

You should see: A JSON response listing your four tools. Something like:

{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"task_add",...},...]}}

If you see an error: Check that dist/index.js exists. If not, run npx tsc again.


Checkpoint 5: Connect to Claude Code

What you're doing: Making your server available to Claude Code.

Get your full path:

echo "$(pwd)/dist/index.js"

Copy that path. Now configure Claude Code to use your server. Run:

claude mcp add task-tracker node /YOUR/FULL/PATH/my-task-tracker/dist/index.js

Replace /YOUR/FULL/PATH/ with the actual path from the echo command.

Alternatively, you can add it to your project's .mcp.json:

{
  "mcpServers": {
    "task-tracker": {
      "command": "node",
      "args": ["/YOUR/FULL/PATH/my-task-tracker/dist/index.js"]
    }
  }
}

Verify Checkpoint 5

In Claude Code, ask:

"What MCP tools do you have available?"

You should see: Claude Code lists your task-tracker tools (task_add, task_list, etc.).

If it's not listed: Check that the path to dist/index.js is correct and absolute. Run claude mcp list to see configured servers.


Checkpoint 6: Use Your Tools

What you're doing: Testing the full flow — intention to execution.

In Claude Code, try these:

Add a task:

"Add a task: review PR #42"

You should see: Claude Code uses task_add and confirms the task was created.

List tasks:

"What's on my task list?"

You should see: The task you just added.

Complete a task:

"Mark the PR review task as done"

You should see: Claude Code uses task_complete and confirms the status change.

Verify the File

Check that tasks are actually persisted:

cat ~/.tasks/tasks.json

You should see: Your tasks in JSON format, with status reflecting your changes.


Checkpoint 7: Reflect

Before marking the capstone complete, answer these questions. Write actual answers — this is part of the work.

1. The Automation Layer

Where does your MCP server sit in the flow from your words to saved tasks? What's above it (the agent)? What's below it (the file system)?

2. The Subtractive Triad

  • DRY: Where would duplication have crept in if you weren't careful?
  • Rams: Why four tools and not five or six? What didn't earn its existence?
  • Heidegger: Does every tool serve the workflow "manage tasks through conversation"?

3. External Memory

Close Claude Code. Open it again. Ask about your tasks. Are they still there? What would break if tasks.json didn't exist?

4. Agent-Native Tools

How did you design for the agent instead of for humans? What makes your tools easy to use?


What You Built

A working automation layer. An AI agent can now manage your tasks without you opening a todo app.

Look at what you have:

  • Four tools with clear boundaries
  • External memory that persists between sessions
  • Structured I/O that agents can parse
  • A system that serves one workflow well

This is Simple Loom — the same patterns that power production task coordination.


Capstone Complete

You've built a working automation layer. Let's verify everything:

Final Checklist

Run these commands to confirm your capstone is complete:

# 1. Check your project structure
ls ~/my-task-tracker/
# Should show: dist/ node_modules/ src/ package.json tsconfig.json

# 2. Check your compiled code
ls ~/my-task-tracker/dist/
# Should show: index.js tasks.js

# 3. Verify tasks persist
cat ~/.tasks/tasks.json
# Should show your tasks in JSON format

In Claude Code, verify the tools work:

Add a task called "Celebrate completing Seeing"

Then:

Show me all my tasks

You should see: Your celebration task listed alongside any others you created.


Course Complete

You've learned:

  • The Meta-Principle: Creation is removal
  • The Automation Layer: What sits between intention and execution
  • The Subtractive Triad: DRY → Rams → Heidegger
  • External Memory: State that survives between sessions
  • Agent-Native Tools: Designing for AI, not humans
  • The Capstone: A working MCP server you built yourself

What you built: An automation layer that persists tasks through conversation. The same pattern powers production systems.


What Comes Next

You've learned to see through the Subtractive Triad. You've built automation infrastructure.

When the three questions become automatic — when you catch yourself asking them without trying — you're ready for tools that execute what you now perceive.

That's the difference between Seeing and Dwelling.


Resources

MCP Documentation

Claude Code

The Subtractive Triad