# Phase 1: Core Capture Pipeline - Research

**Researched:** 2026-04-12
**Domain:** MCP Server + Desktop Capture + Image Grid Compilation
**Confidence:** HIGH

## Summary

Phase 1 builds the complete foundation: an MCP server over stdio that exposes two tools (`start_capture`, `get_capture_status`) and a resource template (`capture://{sessionId}/grid`). The capture engine takes timed desktop screenshots using node-screenshots, stores them in memory, and upon completion compiles them into a JPEG grid image with timestamp overlays using sharp. The grid is constrained to 1568x1568px max (Claude Vision optimal) and returned as base64.

All core libraries are verified as current on npm and ship prebuilt binaries for Windows -- no build tools required. The MCP SDK v1.29.0 provides `registerTool`, `registerResource` with `ResourceTemplate` for dynamic URIs, and `StdioServerTransport`. node-screenshots v0.2.8 provides `Monitor.all()`, `Monitor.captureImage()`, `Window.all()`, and `Window.title()`. sharp v0.34.5 handles resize, composite, and text overlay via its Pango-based `input.text` API.

**Primary recommendation:** Build in strict dependency order -- stdout guard and logger first, then MCP server skeleton with stubs, then capture engine, then grid compiler with timestamps. Each layer is independently testable.

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions
- **D-01:** Async two-tool pattern: `start_capture` returns a session ID immediately, `get_capture_status` returns progress or completed grid image
- **D-02:** `start_capture` accepts: target (desktop for Phase 1), interval_ms, max_frames, duration_ms, jpeg_quality
- **D-03:** `get_capture_status` accepts session_id, returns: status (capturing/complete/error), progress (frames captured / total), and resource URI when complete
- **D-04:** Results exposed as MCP resources at URI `capture://{session_id}/grid` returning base64 JPEG image content
- **D-05:** Structured metadata returned with results: timestamps per frame, capture duration, target info, frame count, grid dimensions
- **D-06:** Individual frames downscaled BEFORE grid assembly to control sharp memory usage
- **D-07:** Grid auto-fits to square-ish layout within 1568x1568px max (Claude Vision optimal size)
- **D-08:** JPEG output at quality 80 by default (configurable via jpeg_quality parameter)
- **D-09:** Grid dimensions auto-calculated: ceil(sqrt(frame_count)) for square-ish layout (e.g., 9 frames = 3x3, 7 frames = 3x3 with 2 empty)
- **D-10:** Relative offset format: "+0.0s", "+0.5s", "+1.0s" (relative to capture start, not wall-clock)
- **D-11:** White text on semi-transparent dark background rectangle for readability on any screenshot
- **D-12:** Positioned at bottom-left of each grid cell
- **D-13:** interval_ms (required): milliseconds between captures
- **D-14:** max_frames (optional, default 20): hard cap on frame count to prevent runaway captures
- **D-15:** duration_ms (optional): total capture duration; if both max_frames and duration_ms set, whichever limit hits first stops capture
- **D-16:** Self-correcting setTimeout pattern (not setInterval) to prevent timer drift over long sessions
- **D-17:** Captures stored in memory as PNG buffers; grid compilation happens once capture completes
- **D-18:** All logging to stderr via a dedicated logger module -- zero stdout outside MCP JSON-RPC
- **D-19:** Stdout guard: override console.log/warn/info to redirect to stderr at server startup
- **D-20:** TypeScript with MCP SDK (@modelcontextprotocol/sdk), StdioServerTransport
- **D-21:** node-screenshots for desktop capture, sharp for image processing

### Claude's Discretion
- Exact sharp concurrency settings for memory management
- Internal buffer format (raw RGBA vs PNG between capture and grid assembly)
- Error message wording and format
- Project scaffolding structure (src/ layout, tsconfig settings)

### Deferred Ideas (OUT OF SCOPE)
None -- discussion stayed within phase scope
</user_constraints>

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|------------------|
| MCP-01 | Server starts via stdio transport and responds to MCP protocol handshake | McpServer + StdioServerTransport from @modelcontextprotocol/sdk v1.29.0; verified API signatures |
| MCP-02 | Server exposes tools for capture configuration and triggering | registerTool API verified; supports inputSchema with zod, title, description, annotations |
| MCP-03 | Server exposes resources for retrieving compiled grid images | ResourceTemplate class supports dynamic URIs like `capture://{sessionId}/grid`; returns BlobResourceContents with base64 blob field |
| MCP-04 | All logging goes to stderr only -- zero stdout pollution | Stdout guard pattern documented; console.log/warn/info override to stderr |
| MCP-05 | Server returns structured metadata alongside results | Tool responses support multiple content items (text + resource_link); metadata in text content |
| CAPT-01 | Capture full desktop screen | Monitor.all() + Monitor.captureImage() from node-screenshots v0.2.8; verified type definitions |
| TIME-01 | Configurable time interval between screenshots | Self-correcting setTimeout pattern; interval_ms parameter |
| TIME-02 | Maximum number of screenshots | max_frames parameter with default 20; checked each tick |
| TIME-03 | Total capture duration alternative | duration_ms parameter; whichever limit (max_frames or duration_ms) hits first stops capture |
| TIME-04 | Self-correcting timers to prevent drift | setTimeout with drift correction based on elapsed time vs expected time |
| GRID-01 | Compile screenshots into single square grid image | sharp create + composite API; verified canvas creation and multi-image compositing |
| GRID-02 | Auto-calculated grid dimensions | ceil(sqrt(frame_count)) formula for cols; ceil(frame_count/cols) for rows |
| GRID-03 | Auto-resize to stay within LLM vision constraints | 1568x1568px max; individual frame downscale before assembly |
| GRID-04 | Timestamp overlay on each grid cell | sharp input.text composite with Pango; white text on dark semi-transparent background |
| GRID-05 | JPEG compression with configurable quality | sharp .jpeg({ quality }) output; default quality 80 |
</phase_requirements>

## Project Constraints (from CLAUDE.md)

CLAUDE.md contains only reference document pointers (claude-code-hooks.md). No project-specific coding conventions, forbidden patterns, or security requirements are established yet -- this phase will set the foundational patterns.

## Standard Stack

### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| @modelcontextprotocol/sdk | 1.29.0 | MCP server framework | Official SDK; McpServer + StdioServerTransport; registerTool/registerResource API [VERIFIED: npm registry] |
| zod | 3.25.6+ or 4.x | Schema validation | Required peer dep of MCP SDK (^3.25 or ^4.0) [VERIFIED: npm registry, MCP SDK package.json] |
| node-screenshots | 0.2.8 | Desktop/window capture | Native NAPI library; Monitor/Window/Image classes; prebuilt binaries for Windows x64 [VERIFIED: npm registry, type definitions] |
| sharp | 0.34.5 | Image processing | libvips-based; composite(), create(), resize(), jpeg(), text input; prebuilt binaries [VERIFIED: npm registry] |

### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| tsx | 4.21.0 | TypeScript execution | Dev-time running without build step [VERIFIED: npm registry] |
| tsup | 8.5.1 | Build/bundle | Production bundling to ESM [VERIFIED: npm registry] |
| @types/node | 20+ | Node.js types | TypeScript support [VERIFIED: npm registry] |

### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| node-screenshots | screenshot-desktop | No window targeting, no region crop, shells out to nircmd on Windows |
| sharp | jimp | Pure JS (no native deps) but 10-50x slower for compositing; viable fallback if sharp install fails |

**Installation:**
```bash
npm install @modelcontextprotocol/sdk zod node-screenshots sharp
npm install -D typescript tsx tsup @types/node
```

**Version verification:** All versions verified against npm registry on 2026-04-12.

## Architecture Patterns

### Recommended Project Structure
```
src/
  index.ts                    # Entry point: stdout guard, create server, connect transport
  server.ts                   # McpServer setup: register tools + resources
  logger.ts                   # Stderr-only logger module
  types.ts                    # Shared TypeScript types and Zod schemas
  capture/
    session-manager.ts        # Session CRUD, lifecycle, auto-cleanup
    scheduler.ts              # Self-correcting setTimeout capture loop
    targets/
      capture-target.ts       # CaptureTarget interface
      desktop-target.ts       # Monitor.captureImage() wrapper (Phase 1)
  processing/
    grid-compiler.ts          # Sharp-based grid assembly + resize
    timestamp-overlay.ts      # Text rendering via sharp input.text
```

### Pattern 1: Stdout Guard (MUST be first code executed)
**What:** Override console.log/warn/info to redirect to stderr before any other module loads
**When to use:** Always -- this is the entry point guard
**Example:**
```typescript
// Source: MCP debugging best practices [CITED: https://modelcontextprotocol.io/docs/tools/debugging]
// Must be FIRST lines in index.ts, before any imports that might trigger console.log
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
process.stdout.write = function(chunk: any, ...args: any[]): boolean {
  // Only allow JSON-RPC messages (start with '{')
  if (typeof chunk === 'string' && chunk.trimStart().startsWith('{')) {
    return originalStdoutWrite(chunk, ...args);
  }
  // Redirect everything else to stderr
  return process.stderr.write(chunk, ...args);
};

// Also redirect console methods
console.log = console.error;
console.warn = console.error;
console.info = console.error;
```

### Pattern 2: MCP Resource Template for Dynamic Session URIs
**What:** Use ResourceTemplate for `capture://{sessionId}/grid` pattern
**When to use:** Resource registration
**Example:**
```typescript
// Source: MCP SDK type definitions [VERIFIED: @modelcontextprotocol/sdk v1.29.0 dist types]
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";

server.registerResource(
  "capture-grid",
  new ResourceTemplate("capture://{sessionId}/grid", {
    list: async () => ({
      resources: activeSessions.map(s => ({
        uri: `capture://${s.id}/grid`,
        name: `Capture ${s.id} Grid`,
        mimeType: "image/jpeg",
      })),
    }),
  }),
  { description: "Grid image for a capture session", mimeType: "image/jpeg" },
  async (uri, { sessionId }) => {
    const session = sessionManager.get(sessionId);
    if (!session || session.state === 'capturing') {
      return { contents: [{ uri: uri.href, mimeType: "text/plain", text: `Capture in progress` }] };
    }
    const gridBuffer = await gridCompiler.compile(session);
    return {
      contents: [{
        uri: uri.href,
        mimeType: "image/jpeg",
        blob: gridBuffer.toString("base64"),
      }],
    };
  }
);
```

### Pattern 3: Self-Correcting Timer
**What:** setTimeout with drift compensation instead of setInterval
**When to use:** Capture scheduling (D-16)
**Example:**
```typescript
// Source: Node.js timer drift issue [CITED: https://github.com/nodejs/node/issues/21822]
function startCapture(config: CaptureConfig, onFrame: (frame: Buffer, elapsed: number) => void) {
  const startTime = Date.now();
  let frameIndex = 0;

  function scheduleNext() {
    frameIndex++;
    const expectedTime = startTime + frameIndex * config.intervalMs;
    const drift = Date.now() - expectedTime;
    const nextDelay = Math.max(0, config.intervalMs - drift);
    setTimeout(captureFrame, nextDelay);
  }

  async function captureFrame() {
    const elapsed = Date.now() - startTime;
    const buffer = await target.capture();
    onFrame(buffer, elapsed);

    if (shouldStop(frameIndex, elapsed, config)) return;
    scheduleNext();
  }

  // First capture immediately
  captureFrame();
}
```

### Pattern 4: Tool Returns Resource Link (not image data)
**What:** start_capture and get_capture_status return text + resource_link, never raw image bytes
**When to use:** All tool responses referencing completed captures
**Example:**
```typescript
// Source: MCP SDK server docs [CITED: https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/server.md]
// When capture is complete:
return {
  content: [
    { type: "text", text: JSON.stringify({
      status: "complete",
      sessionId: session.id,
      frameCount: session.frames.length,
      gridDimensions: { cols, rows },
      timestamps: session.frames.map(f => f.elapsedMs),
      capturedDuration: session.frames[session.frames.length - 1].elapsedMs,
    }) },
    {
      type: "resource_link",
      uri: `capture://${session.id}/grid`,
      name: "Grid Image",
      mimeType: "image/jpeg",
    },
  ],
};
```

### Pattern 5: BlobResourceContents for Binary Image Data
**What:** Resources return binary data via the `blob` field (base64-encoded string)
**When to use:** Grid image resource reads
**Example:**
```typescript
// Source: MCP SDK types [VERIFIED: BlobResourceContentsSchema in SDK types.d.ts]
// BlobResourceContents shape: { uri: string, mimeType?: string, blob: string }
// The blob field is a base64-encoded string
return {
  contents: [{
    uri: uri.href,
    mimeType: "image/jpeg",
    blob: jpegBuffer.toString("base64"),
  }],
};
```

### Anti-Patterns to Avoid
- **Embedding images in tool responses:** Tool results have size limits; always return resource_link URIs instead
- **Synchronous capture-and-return:** start_capture must return immediately with session ID; never block for full capture duration
- **setInterval for capture timing:** Drift accumulates; use self-correcting setTimeout
- **console.log anywhere:** Corrupts MCP stdio transport immediately

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| MCP protocol handling | Custom JSON-RPC parser | @modelcontextprotocol/sdk McpServer | Protocol framing, handshake, error codes are complex |
| Input validation | Manual type checks | zod schemas (required by MCP SDK) | SDK validates automatically from zod schemas |
| Image compositing | Canvas pixel manipulation | sharp composite() | libvips handles memory efficiently, multi-format output |
| Screen capture | Win32 API bindings | node-screenshots | Native NAPI bindings with prebuilt Windows binary |
| UUID generation | Math.random | crypto.randomUUID() | Built into Node.js 19+; cryptographically random |
| Text overlay rendering | Manual pixel drawing | sharp input.text with Pango | Handles font sizing, anti-aliasing, background rectangles |

**Key insight:** Every component in this pipeline has a well-tested library. The project's value is in the orchestration (session lifecycle, grid layout, timer management), not in reimplementing capture or image processing.

## Common Pitfalls

### Pitfall 1: Stdout Corruption Kills MCP Connection
**What goes wrong:** Any non-JSON-RPC output on stdout (console.log, dependency warnings, native binding messages) corrupts the stdio transport and breaks the connection.
**Why it happens:** MCP stdio transport is unusually strict. Libraries like sharp can emit native warnings to stdout.
**How to avoid:** Stdout guard as FIRST code in entry point. Ban console.log. Set `sharp.concurrency(1)` to reduce native thread output. Test by piping stdout to a file.
**Warning signs:** MCP Inspector shows connection dropping immediately after server start. [CITED: https://modelcontextprotocol.io/docs/tools/debugging]

### Pitfall 2: Sharp Text Rendering Deadlocks on Windows
**What goes wrong:** Sharp's Pango-based text rendering can deadlock on Windows when fontconfig scans the Windows font directory under concurrent access.
**Why it happens:** Fontconfig font directory scanning is not thread-safe on Windows. Known issue since sharp 0.31.0.
**How to avoid:** Set `sharp.concurrency(1)` to serialize all sharp operations. Alternatively, create a minimal `fonts.conf` that skips Windows font dir scanning and set `FONTCONFIG_PATH` env var. For timestamp overlays, only one font is needed -- pre-render a test text on startup to warm the font cache before any concurrent operations.
**Warning signs:** Sharp promises that never resolve or reject. Server hangs during grid compilation. [CITED: https://github.com/lovell/sharp/issues/3535]

### Pitfall 3: Grid Images Too Large for LLM Consumption
**What goes wrong:** Full-resolution screenshots produce multi-megabyte grids that exceed MCP resource size limits (~4MB per message) or consume excessive tokens.
**Why it happens:** 1920x1080 screenshots are 2-5MB each as PNG. A 9-frame grid at full resolution would be enormous.
**How to avoid:** Downscale individual frames to thumbnail size BEFORE grid assembly (D-06). Target cell size: ~500px wide. Final grid max 1568x1568px (D-07). JPEG at quality 80 (D-08). Budget: final grid image under 500KB.
**Warning signs:** Grid images larger than 500KB. [CITED: https://github.com/orgs/community/discussions/169224]

### Pitfall 4: Timer Drift in Capture Intervals
**What goes wrong:** setInterval drift accumulates: 20 captures at 500ms takes 11-12s instead of 10s.
**Why it happens:** Node.js setInterval doesn't compensate for callback execution time or event loop delays.
**How to avoid:** Self-correcting setTimeout pattern (D-16) that calculates delay based on expected vs actual elapsed time.
**Warning signs:** Last frame timestamp exceeds expected total duration by >10%. [CITED: https://github.com/nodejs/node/issues/21822]

### Pitfall 5: Sharp Memory Exhaustion During Grid Assembly
**What goes wrong:** Compositing many full-resolution images causes libvips to allocate enormous memory, potentially crashing Node.js.
**Why it happens:** Each 1920x1080 RGBA image is ~8MB uncompressed. 20 frames = 160MB of raw pixel data.
**How to avoid:** Downscale frames on capture (D-06). Set `sharp.concurrency(1)`. Limit max_frames to 20 (D-14). Process grid assembly sequentially.
**Warning signs:** Process memory exceeding 300MB during grid assembly. [CITED: https://github.com/lovell/sharp/issues/138]

## Code Examples

### Complete node-screenshots API (verified from type definitions)
```typescript
// Source: node-screenshots v0.2.8 index.d.ts [VERIFIED: installed and inspected type definitions]
import { Monitor, Window, Image } from "node-screenshots";

// Desktop capture
const monitors = Monitor.all();
const primary = monitors.find(m => m.isPrimary());
const image: Image = await primary.captureImage();
const pngBuffer: Buffer = await image.toPng();
const jpegBuffer: Buffer = await image.toJpeg();

// Monitor properties
primary.id();          // number
primary.name();        // string
primary.width();       // number (pixel width)
primary.height();      // number (pixel height)
primary.scaleFactor(); // number (DPI scale, e.g., 1.5 for 150%)
primary.isPrimary();   // boolean

// Window enumeration (Phase 2, but API verified here)
const windows = Window.all();
windows[0].title();       // string - window title
windows[0].appName();     // string - app name
windows[0].pid();         // number - process ID
windows[0].isMinimized(); // boolean
windows[0].isMaximized(); // boolean
windows[0].isFocused();   // boolean
```

### Grid Compilation with Thumbnail Downscale
```typescript
// Source: sharp composite API [CITED: https://sharp.pixelplumbing.com/api-composite]
import sharp from "sharp";

async function compileGrid(
  frames: { buffer: Buffer; elapsedMs: number }[],
  maxEdge: number = 1568,
  jpegQuality: number = 80,
): Promise<Buffer> {
  const cols = Math.ceil(Math.sqrt(frames.length));
  const rows = Math.ceil(frames.length / cols);

  // Calculate cell size to fit within maxEdge
  const cellWidth = Math.floor(maxEdge / cols);
  const cellHeight = Math.floor(maxEdge / rows);
  const canvasWidth = cellWidth * cols;
  const canvasHeight = cellHeight * rows;

  // Resize all frames to cell size
  const resized = await Promise.all(
    frames.map(f => sharp(f.buffer).resize(cellWidth, cellHeight, { fit: "fill" }).toBuffer())
  );

  // Build composite array
  const composites = resized.map((buf, i) => ({
    input: buf,
    left: (i % cols) * cellWidth,
    top: Math.floor(i / cols) * cellHeight,
  }));

  return sharp({
    create: {
      width: canvasWidth,
      height: canvasHeight,
      channels: 4 as const,
      background: { r: 0, g: 0, b: 0, alpha: 1 },
    },
  })
    .composite(composites)
    .jpeg({ quality: jpegQuality })
    .toBuffer();
}
```

### Timestamp Overlay via sharp input.text
```typescript
// Source: sharp constructor text options [CITED: https://sharp.pixelplumbing.com/api-constructor]
import sharp from "sharp";

async function createTimestampOverlay(
  text: string,        // e.g., "+1.5s"
  cellWidth: number,
  cellHeight: number,
): Promise<Buffer> {
  const fontSize = Math.max(12, Math.floor(cellHeight * 0.06));
  const dpi = Math.round(fontSize * 72 / 12); // Scale DPI to achieve target font size

  // Create text image with semi-transparent dark background
  const textImage = await sharp({
    text: {
      text: `<span foreground="white">${text}</span>`,
      font: "sans",
      dpi: dpi,
      rgba: true,
    },
  }).toBuffer({ resolveWithObject: true });

  // Create background rectangle
  const padding = 4;
  const bgWidth = textImage.info.width + padding * 2;
  const bgHeight = textImage.info.height + padding * 2;

  const overlay = await sharp({
    create: {
      width: bgWidth,
      height: bgHeight,
      channels: 4,
      background: { r: 0, g: 0, b: 0, alpha: 0.6 },
    },
  })
    .composite([{ input: textImage.data, left: padding, top: padding }])
    .png()
    .toBuffer();

  return overlay;
}
```

### MCP Server Skeleton
```typescript
// Source: MCP SDK [VERIFIED: @modelcontextprotocol/sdk v1.29.0 type definitions]
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "screen-timelapse",
  version: "0.1.0",
});

// Register tool with new API
server.registerTool("start_capture", {
  title: "Start Screen Capture",
  description: "Begin a timed desktop capture session",
  inputSchema: {
    interval_ms: z.number().min(100).describe("Milliseconds between captures"),
    max_frames: z.number().min(1).max(50).default(20).describe("Maximum frames to capture"),
    duration_ms: z.number().optional().describe("Total capture duration in ms"),
    jpeg_quality: z.number().min(1).max(100).default(80).describe("JPEG quality for output"),
  },
}, async (args) => {
  // Implementation here
  return { content: [{ type: "text", text: "..." }] };
});

const transport = new StdioServerTransport();
await server.connect(transport);
```

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| `server.tool()` / `server.resource()` | `server.registerTool()` / `server.registerResource()` | MCP SDK recent versions | Old API deprecated but still works; use registerTool/registerResource for new code [VERIFIED: SDK types show @deprecated on old methods] |
| SVG overlay for text | `input.text` with Pango markup | sharp 0.32+ | Pango text is more reliable than SVG text on Windows [CITED: sharp docs] |
| setInterval for timed capture | Self-correcting setTimeout | Best practice | Prevents drift accumulation over long sessions |
| zod v3 | zod v3.25+ or v4 | 2025 | MCP SDK requires ^3.25 or ^4.0 as peer dependency [VERIFIED: SDK package.json] |

**Deprecated/outdated:**
- `McpServer.tool()` / `.resource()` / `.prompt()`: Deprecated in favor of `registerTool()` / `registerResource()` / `registerPrompt()` [VERIFIED: SDK type definitions]

## Assumptions Log

| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | sharp input.text with Pango works reliably on Windows 11 at concurrency(1) | Pitfall 2, Code Examples | Would need to fall back to SVG overlay or pre-rendered PNG stamps; moderate risk |
| A2 | node-screenshots handles DPI scaling correctly on Windows 11 at non-100% scaling | Architecture | Screenshots could be wrong size; would need manual DPI factor compensation |
| A3 | MCP resource_link content type is supported by Claude Code as a client | Pattern 4 | Would need to return base64 image directly in tool response instead; design change |
| A4 | 1568x1568px JPEG at quality 80 stays under MCP message size limits (~4MB) | Pitfall 3 | Would need to reduce resolution or quality further |

## Open Questions

1. **Sharp text rendering reliability on Windows 11**
   - What we know: Known deadlock issues with fontconfig on Windows (fixed by concurrency(1) or FONTCONFIG_PATH workaround). STATE.md flagged this for verification.
   - What's unclear: Whether `input.text` API at `sharp.concurrency(1)` works reliably in this specific Windows 11 environment.
   - Recommendation: Build it with input.text, test early. Fallback plan: generate timestamp images as simple colored rectangles with no text (degrade gracefully).

2. **DPI scaling behavior at 150%**
   - What we know: node-screenshots claims DPI-aware capture. Monitor.scaleFactor() returns the DPI scale. STATE.md flagged for hands-on verification.
   - What's unclear: Whether captured image dimensions are physical pixels or logical pixels at non-100% scaling.
   - Recommendation: Capture a screenshot at current DPI and log dimensions vs Monitor.width()/height() in early development. Adjust if needed.

3. **MCP resource_link support in Claude Code**
   - What we know: The MCP spec supports resource_link in tool responses. The SDK supports it.
   - What's unclear: Whether Claude Code (the primary client) actually follows resource_link URIs to fetch the resource.
   - Recommendation: Implement resource_link as primary. As fallback, also support returning the grid image inline as base64 image content in the get_capture_status tool response if under size limits.

## Environment Availability

| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
| Node.js | Runtime | Yes | v25.8.2 | -- |
| TypeScript | Language | Yes | 6.0.2 | -- |
| npm | Package manager | Yes | (bundled with Node) | -- |
| Git | Version control | Not checked | -- | Not blocking |

**Notes:**
- Node.js v25.8.2 is installed, which is newer than the recommended v20 LTS. node-screenshots and sharp prebuilt binaries should work with Node 25 (NAPI is version-independent), but this should be verified during npm install. [ASSUMED]
- TypeScript 6.0.2 is available globally. The project will use a local install pinned to a specific version.

**Missing dependencies with no fallback:** None

## Security Domain

This phase has minimal security surface -- it is a local-only MCP server over stdio. No network listeners, no authentication, no user-facing UI.

### Applicable ASVS Categories

| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | No | Local stdio only |
| V3 Session Management | No | In-memory sessions, no persistence |
| V4 Access Control | No | Single-user local tool |
| V5 Input Validation | Yes | zod schemas on all tool inputs (enforced by MCP SDK) |
| V6 Cryptography | No | No secrets or encryption needed |

### Known Threat Patterns

| Pattern | STRIDE | Standard Mitigation |
|---------|--------|---------------------|
| Malformed tool input | Tampering | zod schema validation (automatic via MCP SDK) |
| Unbounded resource consumption | Denial of Service | max_frames cap (20), session TTL, memory limits |
| Stdout injection | Information Disclosure | Stdout guard redirects all non-JSON-RPC to stderr |

## Sources

### Primary (HIGH confidence)
- [@modelcontextprotocol/sdk v1.29.0](https://www.npmjs.com/package/@modelcontextprotocol/sdk) - npm registry version verified
- [MCP SDK type definitions](https://github.com/modelcontextprotocol/typescript-sdk) - McpServer, ResourceTemplate, registerTool/registerResource API inspected from installed package
- [node-screenshots v0.2.8 type definitions](https://github.com/nashaofu/node-screenshots) - Complete Monitor/Window/Image API verified by installing package and reading index.d.ts
- [sharp v0.34.5 official docs](https://sharp.pixelplumbing.com/) - composite, constructor (create + text), resize, jpeg APIs
- [sharp composite API](https://sharp.pixelplumbing.com/api-composite) - Multi-image compositing patterns
- [zod v3.25.6+ / v4.x](https://www.npmjs.com/package/zod) - MCP SDK peer dependency verified from package.json

### Secondary (MEDIUM confidence)
- [Sharp issue #3535](https://github.com/lovell/sharp/issues/3535) - Windows SVG/text rendering deadlock; closed with workaround
- [Sharp issue #138](https://github.com/lovell/sharp/issues/138) - Memory issues with many images
- [MCP debugging guide](https://modelcontextprotocol.io/docs/tools/debugging) - Stdout corruption diagnostics
- [Node.js timer drift issue](https://github.com/nodejs/node/issues/21822) - setInterval drift documentation

### Tertiary (LOW confidence)
- [MCP large output discussion](https://github.com/orgs/community/discussions/169224) - Resource size limits (~4MB)

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH - All versions verified against npm registry; type definitions inspected from installed packages
- Architecture: HIGH - MCP SDK API patterns verified from type definitions; node-screenshots API fully documented
- Pitfalls: HIGH - All critical pitfalls verified via GitHub issues and official documentation
- Text overlay on Windows: MEDIUM - Known issues exist but workarounds documented; needs hands-on verification

**Research date:** 2026-04-12
**Valid until:** 2026-05-12 (30 days -- stable ecosystem, libraries at mature versions)
