Skip to main content
Glama
by wei
data-model.md15.4 kB
# Data Model Specification **Feature**: HackerNews MCP Server **Date**: October 12, 2025 **Status**: Complete ## Overview This document defines all entities, their fields, relationships, validation rules, and state transitions for the HackerNews MCP Server. These models align with the HackerNews Algolia API response schema and MCP protocol requirements. --- ## Entity Definitions ### 1. HNItem (Base Entity) The base entity representing any item in HackerNews (story, comment, poll, etc.). **Fields**: | Field | Type | Required | Description | Validation | |-------|------|----------|-------------|------------| | `objectID` | `string` | Yes | Unique identifier for the item | Non-empty string | | `created_at` | `string` | Yes | ISO 8601 timestamp of creation | Valid ISO date | | `created_at_i` | `number` | Yes | Unix timestamp (seconds since epoch) | Positive integer | | `author` | `string` | Yes | Username of item creator | Non-empty string | | `title` | `string \| null` | No | Title of the story/poll | Max 200 chars | | `url` | `string \| null` | No | URL of the linked content | Valid URL or null | | `text` | `string \| null` | No | Text content (for Ask HN, comments) | No max length | | `points` | `number \| null` | No | Number of upvotes | Non-negative integer or null | | `parent_id` | `number \| null` | No | ID of parent item (for comments) | Positive integer or null | | `story_id` | `number \| null` | No | ID of the story (for comments) | Positive integer or null | | `children` | `HNItem[]` | No | Array of nested child items | Array of HNItem | | `num_comments` | `number \| null` | No | Total comment count | Non-negative integer or null | | `_tags` | `string[]` | Yes | Array of tags (story, comment, etc.) | Non-empty array | | `_highlightResult` | `object \| null` | No | Search highlighting information | Complex object | **Relationships**: - **Parent-Child**: `parent_id` references another `HNItem.objectID` - **Story-Comment**: `story_id` references the root story's `objectID` - **Children**: Nested array of child items (comments, replies) **Validation Rules**: ```typescript const HNItemSchema = z.object({ objectID: z.string().min(1), created_at: z.string().datetime(), created_at_i: z.number().int().positive(), author: z.string().min(1), title: z.string().max(200).nullable(), url: z.string().url().nullable(), text: z.string().nullable(), points: z.number().int().nonnegative().nullable(), parent_id: z.number().int().positive().nullable(), story_id: z.number().int().positive().nullable(), children: z.array(z.lazy(() => HNItemSchema)).default([]), num_comments: z.number().int().nonnegative().nullable(), _tags: z.array(z.string()).min(1), _highlightResult: z.record(z.any()).nullable() }); ``` **Usage Contexts**: - Returned by `search` and `search_by_date` endpoints - Returned by `items/:id` endpoint with nested children - Base type for specialized item types --- ### 2. HNStory (Specialized HNItem) Represents a story post on HackerNews. **Additional Constraints**: - `_tags` MUST include `"story"` - `title` MUST NOT be null - `url` OR `text` MUST be present (link post vs text post) - `points` SHOULD be present - `num_comments` SHOULD be present **Distinguishing Features**: - Has `title` and optionally `url` - Top-level item (no `parent_id`) - Can have comments as children **TypeScript Type**: ```typescript type HNStory = HNItem & { _tags: string[] & { includes: 'story' }; title: string; points: number; num_comments: number; }; ``` --- ### 3. HNComment (Specialized HNItem) Represents a comment or reply on HackerNews. **Additional Constraints**: - `_tags` MUST include `"comment"` - `text` MUST NOT be null - `parent_id` MUST be present - `story_id` MUST be present - `points` MAY be present **Distinguishing Features**: - Has `text` content - Has `parent_id` linking to parent comment or story - Has `story_id` linking to root story - Can have nested reply comments in `children` **TypeScript Type**: ```typescript type HNComment = HNItem & { _tags: string[] & { includes: 'comment' }; text: string; parent_id: number; story_id: number; }; ``` --- ### 4. HNUser Represents a HackerNews user profile. **Fields**: | Field | Type | Required | Description | Validation | |-------|------|----------|-------------|------------| | `username` | `string` | Yes | Unique username | Non-empty, alphanumeric + underscore | | `karma` | `number` | Yes | User's karma score | Non-negative integer | | `about` | `string \| null` | No | User bio/about text (optional) | Max 5000 chars | **Validation Rules**: ```typescript const HNUserSchema = z.object({ username: z.string().min(1).regex(/^[a-zA-Z0-9_]+$/), karma: z.number().int().nonnegative(), about: z.string().max(5000).nullable().optional() }); ``` **Usage Contexts**: - Returned by `/users/:username` endpoint - Used to enrich author information in UI contexts **Relationships**: - One user can create many items (`HNItem.author`) - No direct relationship stored in user object --- ### 5. SearchResult Represents the response from search operations (both `search` and `search_by_date`). **Fields**: | Field | Type | Required | Description | Validation | |-------|------|----------|-------------|------------| | `hits` | `HNItem[]` | Yes | Array of matching items | Array of valid HNItems | | `nbHits` | `number` | Yes | Total number of hits | Non-negative integer | | `page` | `number` | Yes | Current page number (0-indexed) | Non-negative integer | | `nbPages` | `number` | Yes | Total number of pages | Positive integer | | `hitsPerPage` | `number` | Yes | Number of hits per page | Positive integer (typically 20) | | `processingTimeMS` | `number` | Yes | Query processing time in milliseconds | Non-negative number | | `query` | `string` | Yes | The search query used | Can be empty string | | `params` | `string` | Yes | URL-encoded query parameters | Non-empty string | **Validation Rules**: ```typescript const SearchResultSchema = z.object({ hits: z.array(HNItemSchema), nbHits: z.number().int().nonnegative(), page: z.number().int().nonnegative(), nbPages: z.number().int().positive(), hitsPerPage: z.number().int().positive(), processingTimeMS: z.number().nonnegative(), query: z.string(), params: z.string().min(1) }); ``` **Usage Contexts**: - Returned by `search-posts` tool - Returned by `get-latest-posts` tool - Returned by `get-front-page` tool **Pagination Rules**: - Pages are 0-indexed (`page: 0` is first page) - `page` MUST be less than `nbPages` - Last page may have fewer than `hitsPerPage` items --- ### 6. ItemResult Represents the response from fetching a single item with nested children. **Fields**: | Field | Type | Required | Description | Validation | |-------|------|----------|-------------|------------| | `id` | `string` | Yes | Item ID (converted from number by client) | Non-empty string | | `created_at` | `string` | Yes | ISO 8601 timestamp | Valid ISO date | | `created_at_i` | `number` | Yes | Unix timestamp | Positive integer | | `type` | `string` | Yes | Item type (story, comment, poll) | One of: story, comment, poll, pollopt | | `author` | `string` | Yes | Creator username | Non-empty string | | `title` | `string \| null` | No | Item title (for stories, null for comments) | Max 200 chars | | `url` | `string \| null` | No | URL (for link stories, null otherwise) | Valid URL or null | | `text` | `string \| null` | No | Text content (for comments/text posts) | No max length | | `points` | `number \| null` | No | Upvote count (null for some types) | Non-negative integer or null | | `parent_id` | `number \| null` | No | Parent item ID (null for top-level) | Positive integer or null | | `story_id` | `number` | Yes | Story ID (same as id for stories) | Positive integer | | `options` | `number[]` | Yes | Options array (for polls, empty otherwise) | Array of integers | | `children` | `ItemResult[]` | No | Nested children (full tree) | Array of ItemResult | **Note**: The HackerNews API returns `id` as a number, but the client converts it to string for consistency with the search API's `objectID` field. **Validation Rules**: ```typescript const ItemResultSchema = z.object({ id: z.string().min(1), created_at: z.string().datetime(), created_at_i: z.number().int().positive(), type: z.enum(['story', 'comment', 'poll', 'pollopt']), author: z.string().min(1), title: z.string().max(200).nullable(), url: z.string().url().nullable(), text: z.string().nullable(), points: z.number().int().nonnegative().nullable(), parent_id: z.number().int().positive().nullable(), story_id: z.number().int().positive(), options: z.array(z.number().int()), children: z.array(z.lazy(() => ItemResultSchema)).default([]) }); ``` **Usage Contexts**: - Returned by `get-item` tool - Contains complete nested comment tree - Used for displaying full discussion threads **Tree Structure**: - `children` array contains full recursive tree - No depth limit (can be arbitrarily nested) - Depth-first traversal for display --- ## Tool Input/Output Schemas ### Tool: `search-posts` **Input Schema**: ```typescript const SearchPostsInput = z.object({ query: z.string().min(1).describe("Search query text"), tags: z.array(z.string()).optional().describe("Filter tags (story, comment, etc.)"), numericFilters: z.array(z.string()).optional().describe("Numeric filters (points>=100)"), page: z.number().int().nonnegative().default(0).describe("Page number (0-indexed)"), hitsPerPage: z.number().int().min(1).max(1000).default(20).describe("Results per page") }); ``` **Output Schema**: ```typescript const SearchPostsOutput = SearchResultSchema; ``` **Validation Notes**: - `query` cannot be empty (minimum 1 character) - `page` must be non-negative integer - `hitsPerPage` must be between 1 and 1000 (API limit) - `tags` and `numericFilters` are optional arrays --- ### Tool: `get-front-page` **Input Schema**: ```typescript const GetFrontPageInput = z.object({ page: z.number().int().nonnegative().default(0).describe("Page number (0-indexed)"), hitsPerPage: z.number().int().min(1).max(1000).default(30).describe("Results per page") }); ``` **Output Schema**: ```typescript const GetFrontPageOutput = SearchResultSchema; ``` **Validation Notes**: - Always uses `tags=front_page` filter internally - Pagination works same as search --- ### Tool: `get-latest-posts` **Input Schema**: ```typescript const GetLatestPostsInput = z.object({ tags: z.array(z.string()).optional().describe("Filter by tags (story, comment)"), page: z.number().int().nonnegative().default(0).describe("Page number (0-indexed)"), hitsPerPage: z.number().int().min(1).max(1000).default(20).describe("Results per page") }); ``` **Output Schema**: ```typescript const GetLatestPostsOutput = SearchResultSchema; ``` **Validation Notes**: - Uses `search_by_date` endpoint internally - Empty query to get all latest posts - Optional tag filtering --- ### Tool: `get-item` **Input Schema**: ```typescript const GetItemInput = z.object({ itemId: z.string().min(1).describe("HackerNews item ID") }); ``` **Output Schema**: ```typescript const GetItemOutput = ItemResultSchema; ``` **Validation Notes**: - `itemId` must be non-empty string - Returns 404-style error if item doesn't exist - Includes full nested comment tree in response --- ### Tool: `get-user` **Input Schema**: ```typescript const GetUserInput = z.object({ username: z.string().min(1).regex(/^[a-zA-Z0-9_]+$/).describe("HackerNews username") }); ``` **Output Schema**: ```typescript const GetUserOutput = HNUserSchema; ``` **Validation Notes**: - Username must be alphanumeric plus underscore - Returns error if user doesn't exist - No pagination (single user profile) --- ## State Transitions ### Item Lifecycle Items in HackerNews are immutable from the API perspective. The MCP server does not track state transitions as all data is read-only. **States** (from API perspective): 1. **Published**: Item exists and is accessible via API 2. **Deleted**: Item may return null or be inaccessible 3. **Dead**: Item flagged but structure remains **No State Transitions in MCP Server**: - Server is read-only - Does not cache or persist state - Always fetches fresh data from API - No state management needed --- ## Validation Error Handling ### Input Validation Errors When input validation fails, the tool should return: ```typescript { content: [{ type: 'text', text: `Validation error: ${error.message}` }], isError: true } ``` **Common Validation Errors**: - Empty query string - Invalid page number (negative) - Invalid hitsPerPage (out of range 1-1000) - Invalid username format - Invalid item ID format --- ### API Response Validation When API response doesn't match expected schema: ```typescript { content: [{ type: 'text', text: `API response validation failed: ${error.message}` }], isError: true } ``` **Handling Strategies**: 1. **Partial Data**: Accept partial data if core fields present 2. **Missing Optional Fields**: Use null/undefined for missing optionals 3. **Type Coercion**: Coerce compatible types (string to number where safe) 4. **Unknown Fields**: Ignore extra fields from API --- ## Entity Relationships Diagram ```mermaid classDiagram class HNUser { +username +karma +about +created } class HNItem { +objectID +author +created_at +title +url? +text? +points? +parent_id? +story_id? +children[] } class HNStory { +title* +points* +url? } class HNComment { +text* +parent_id* +story_id* } HNUser --> HNItem : authors HNItem --> HNItem : parent_id (self) HNItem <|-- HNStory HNItem <|-- HNComment ``` **Legend**: - `*`: Required field for derived type - `?`: Optional field - `children[]`: Array of child items --- ## API Endpoint Mapping | Tool | API Endpoint | Primary Entity | |------|--------------|----------------| | `search-posts` | `GET /search` | `SearchResult` | | `get-front-page` | `GET /search?tags=front_page` | `SearchResult` | | `get-latest-posts` | `GET /search_by_date` | `SearchResult` | | `get-item` | `GET /items/:id` | `ItemResult` | | `get-user` | `GET /users/:username` | `HNUser` | --- ## Schema Evolution Strategy **Versioning Approach**: - MCP server version follows SemVer - Breaking schema changes increment MAJOR version - New optional fields increment MINOR version - Bug fixes increment PATCH version **Backward Compatibility**: - All schema changes must be backward compatible - New fields added as optional - Deprecated fields marked but not removed - Support at least N-1 version of HackerNews API **Testing Strategy**: - Contract tests verify schema compliance - Integration tests use real API responses - Mock responses for edge cases - Regression tests for each schema version --- ## Summary **Core Entities**: 6 defined (HNItem, HNStory, HNComment, HNUser, SearchResult, ItemResult) **Validation**: Zod schemas for all entities and tool inputs/outputs **Relationships**: Parent-child for items, author for users **State**: Read-only, no state management needed **Testing**: 100% schema coverage required per Constitution **Next Phase**: Generate API contracts and tool implementations

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wei/hn-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server