# Data Model: HackerNews MCP Server
**Feature**: 001-hackernews-mcp-server
**Date**: 2025-10-12
## Overview
This document defines the data structures used by the HackerNews MCP Server. All entities are derived from the HN Algolia API responses and represent read-only views of Hacker News content. No data persistence is required as this is a stateless API client.
## Core Entities
### Story
Represents a Hacker News story (post with URL or Ask HN text post).
**Fields**:
- `id`: number - Unique story identifier
- `title`: string - Story title
- `url`: string | null - External URL (null for Ask HN)
- `author`: string - Username of submitter
- `points`: number - Upvote count
- `story_text`: string | null - Text content (for Ask HN posts)
- `comment_text`: null - Always null for stories
- `created_at`: string - ISO timestamp
- `created_at_i`: number - Unix timestamp in seconds
- `num_comments`: number - Total comment count
- `objectID`: string - Algolia object ID (same as id)
- `_tags`: string[] - Content tags (e.g., ["story", "author_pg"])
- `_highlightResult`: object | undefined - Search highlighting metadata
**Relationships**:
- Has many Comments (via children/kids)
- Belongs to one User (author)
**Validation Rules**:
- `id` must be positive integer
- `title` must not be empty
- `author` must not be empty
- `points` >= 0
- `num_comments` >= 0
- `created_at_i` must be valid Unix timestamp
**State Transitions**: N/A (read-only)
---
### Comment
Represents a user comment on a story or reply to another comment.
**Fields**:
- `id`: number - Unique comment identifier
- `author`: string - Username of commenter
- `text`: string - Comment text (HTML)
- `comment_text`: string - Plain text version
- `story_text`: null - Always null for comments
- `points`: number | null - Upvote count (may be null)
- `parent_id`: number - ID of parent story or comment
- `story_id`: number - ID of root story
- `created_at`: string - ISO timestamp
- `created_at_i`: number - Unix timestamp in seconds
- `objectID`: string - Algolia object ID
- `_tags`: string[] - Content tags (e.g., ["comment", "author_sama"])
- `children`: Comment[] | undefined - Nested replies
**Relationships**:
- Belongs to one Story (via story_id)
- Belongs to one User (author)
- Belongs to one Comment or Story (via parent_id)
- Has many Comments (nested children)
**Validation Rules**:
- `id` must be positive integer
- `author` must not be empty
- `text` or `comment_text` must not be empty
- `parent_id` must be valid ID
- `story_id` must be valid ID
- `created_at_i` must be valid Unix timestamp
**State Transitions**: N/A (read-only)
---
### User
Represents a Hacker News user profile.
**Fields**:
- `username`: string - Unique username
- `about`: string | null - User bio (HTML)
- `karma`: number - Total karma points
- `created_at`: string - ISO timestamp of account creation
- `created_at_i`: number - Unix timestamp in seconds
**Relationships**:
- Has many Stories (submitted)
- Has many Comments (submitted)
**Validation Rules**:
- `username` must not be empty
- `username` must match pattern: alphanumeric, hyphens, underscores
- `karma` >= 0
- `created_at_i` must be valid Unix timestamp
**State Transitions**: N/A (read-only)
---
### Poll
Represents a poll post (story with poll options).
**Fields**:
- Inherits all Story fields
- `poll_options`: PollOption[] - Array of poll choices
**Relationships**:
- Has many PollOptions
- Inherits Story relationships
**Validation Rules**:
- Inherits Story validation rules
- Must have at least 2 poll options
---
### PollOption
Represents a single option in a poll.
**Fields**:
- `id`: number - Unique option identifier
- `text`: string - Option text
- `points`: number - Vote count for this option
- `parent_id`: number - ID of parent poll
- `created_at_i`: number - Unix timestamp in seconds
**Relationships**:
- Belongs to one Poll
**Validation Rules**:
- `id` must be positive integer
- `text` must not be empty
- `points` >= 0
- `parent_id` must be valid poll ID
---
### SearchResult
Container for search responses with pagination metadata.
**Fields**:
- `hits`: Story[] | Comment[] - Array of results
- `nbHits`: number - Total number of matching results
- `nbPages`: number - Total number of pages
- `page`: number - Current page number (0-indexed)
- `hitsPerPage`: number - Results per page
- `processingTimeMS`: number - Search processing time
- `query`: string - Original search query
- `params`: string - URL query parameters used
**Relationships**:
- Contains many Stories or Comments (polymorphic)
**Validation Rules**:
- `hits` must be an array
- `nbHits` >= 0
- `nbPages` >= 0
- `page` >= 0
- `hitsPerPage` > 0
- `processingTimeMS` >= 0
---
## Type Definitions (TypeScript)
```typescript
// Base HN Item types
export interface HNStory {
id: number;
title: string;
url: string | null;
author: string;
points: number;
story_text: string | null;
comment_text: null;
created_at: string;
created_at_i: number;
num_comments: number;
objectID: string;
_tags: string[];
_highlightResult?: Record<string, unknown>;
}
export interface HNComment {
id: number;
author: string;
text: string;
comment_text: string;
story_text: null;
points: number | null;
parent_id: number;
story_id: number;
created_at: string;
created_at_i: number;
objectID: string;
_tags: string[];
children?: HNComment[];
}
export interface HNUser {
username: string;
about: string | null;
karma: number;
created_at: string;
created_at_i: number;
}
export interface HNPollOption {
id: number;
text: string;
points: number;
parent_id: number;
created_at_i: number;
}
export interface HNSearchResult<T = HNStory | HNComment> {
hits: T[];
nbHits: number;
nbPages: number;
page: number;
hitsPerPage: number;
processingTimeMS: number;
query: string;
params: string;
}
// Item response (includes nested comments)
export interface HNItemResponse {
id: number;
created_at: string;
author: string;
title?: string;
url?: string;
text?: string;
points: number;
parent_id: number | null;
children?: HNItemResponse[];
}
// User response
export interface HNUserResponse {
username: string;
about: string | null;
karma: number;
}
```
## Query Parameters
### Search Parameters
Used for all search endpoints (`/search` and `/search_by_date`):
```typescript
export interface SearchParams {
query: string; // Search text
tags?: string; // Filter tags (comma-separated or parentheses for OR)
numericFilters?: string; // Numeric filters (e.g., "created_at_i>X,points>=Y")
page?: number; // Page number (0-indexed)
hitsPerPage?: number; // Results per page (default 20)
restrictSearchableAttributes?: string; // Limit search to specific fields
}
```
### Available Tags
- Content types: `story`, `comment`, `poll`, `pollopt`
- Special: `show_hn`, `ask_hn`, `front_page`
- Author filter: `author_{username}`
- Story filter: `story_{id}`
### Numeric Filters
- `created_at_i`: Unix timestamp (supports >, >=, =, <=, <)
- `points`: Score (supports >, >=, =, <=, <)
- `num_comments`: Comment count (supports >, >=, =, <=, <)
**Example Filters**:
```
created_at_i>1640000000 // After timestamp
points>=100 // Minimum 100 points
num_comments>=50 // At least 50 comments
created_at_i>X,created_at_i<Y // Between timestamps (comma = AND)
```
## Error Types
### API Errors
```typescript
export class HNAPIError extends Error {
constructor(
message: string,
public readonly statusCode: number,
public readonly response: unknown
) {
super(message);
this.name = 'HNAPIError';
}
}
export class RateLimitError extends Error {
constructor(
public readonly current: number,
public readonly limit: number
) {
super(`Rate limit exceeded: ${current}/${limit} requests per hour`);
this.name = 'RateLimitError';
}
}
export class ValidationError extends Error {
constructor(
message: string,
public readonly field: string,
public readonly value: unknown
) {
super(message);
this.name = 'ValidationError';
}
}
```
## Data Flow
```
MCP Client (Claude)
↓
MCP Tool (search_stories)
↓
HN Client (hn-client.ts)
↓
Rate Limiter (check limit)
↓
HTTP Request (fetch API)
↓
HN Algolia API
↓
JSON Response
↓
Type Validation (TypeScript)
↓
MCP Response (structured content)
↓
MCP Client (Claude)
```
## Notes
- All timestamps are provided in both ISO string and Unix formats
- HTML content in `text` and `about` fields should be sanitized on display
- Nested comments can be deeply nested (recursive structure)
- Search highlighting is optional and only present when query matches
- Empty results return `hits: []` with `nbHits: 0`
- API may return partial data for very old items (handle null fields gracefully)