# MCP Tool Contracts Specification
**Feature**: HackerNews MCP Server
**Date**: October 12, 2025
**Status**: Complete
## Overview
This document defines the complete contracts for all MCP tools exposed by the HackerNews MCP server. Each tool definition includes its purpose, input parameters, output format, error conditions, and usage examples.
---
## Tool Definitions
### 1. search-posts
**Purpose**: Search HackerNews posts by query with optional filters and pagination.
**MCP Registration**:
```typescript
server.registerTool(
'search-posts',
{
title: 'Search HackerNews Posts',
description: 'Search HackerNews for stories, comments, and other content by keyword. Supports filtering by tags, numeric filters (points, comments, date), and pagination. Returns sorted results by relevance or date.',
inputSchema: {
query: z.string().min(1).describe('Search query text. Use keywords to find relevant posts.'),
tags: z.array(z.string()).optional().describe('Optional filter tags. Examples: ["story"], ["comment"], ["story", "front_page"]'),
numericFilters: z.array(z.string()).optional().describe('Optional numeric filters. Examples: ["points>=100"], ["created_at_i>1640000000"]'),
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 (1-1000)')
},
outputSchema: {
hits: z.array(z.any()),
nbHits: z.number(),
page: z.number(),
nbPages: z.number(),
hitsPerPage: z.number(),
processingTimeMS: z.number(),
query: z.string(),
params: z.string()
}
},
async (params) => { /* implementation */ }
);
```
**Input Parameters**:
| Parameter | Type | Required | Default | Description | Examples |
|-----------|------|----------|---------|-------------|----------|
| `query` | `string` | Yes | - | Search keywords | `"AI"`, `"Python tutorial"`, `"YC startup"` |
| `tags` | `string[]` | No | `undefined` | Filter by type/category | `["story"]`, `["comment"]`, `["story", "front_page"]` |
| `numericFilters` | `string[]` | No | `undefined` | Numeric comparisons | `["points>=100"]`, `["created_at_i>1640000000", "num_comments>=50"]` |
| `page` | `number` | No | `0` | Page number (0-indexed) | `0`, `1`, `2` |
| `hitsPerPage` | `number` | No | `20` | Results per page | `10`, `20`, `50`, `100` |
**Output Format**:
```typescript
{
hits: HNItem[], // Array of matching items
nbHits: number, // Total matches across all pages
page: number, // Current page (0-indexed)
nbPages: number, // Total pages available
hitsPerPage: number, // Items per page
processingTimeMS: number, // Query execution time
query: string, // The search query used
params: string // URL-encoded parameters
}
```
**Success Example**:
```json
{
"hits": [
{
"objectID": "123456",
"created_at": "2025-10-12T10:30:00.000Z",
"created_at_i": 1728732600,
"author": "pg",
"title": "The Rise of AI Startups",
"url": "https://example.com/ai-startups",
"points": 342,
"num_comments": 87,
"_tags": ["story", "front_page"]
}
],
"nbHits": 1542,
"page": 0,
"nbPages": 78,
"hitsPerPage": 20,
"processingTimeMS": 12,
"query": "AI",
"params": "query=AI&tags=story"
}
```
**Error Conditions**:
| Error Type | HTTP Code | Condition | Example Message |
|------------|-----------|-----------|-----------------|
| Validation | 400 | Empty query | `"Validation error: query must contain at least 1 character"` |
| Validation | 400 | Invalid page | `"Validation error: page must be non-negative"` |
| Validation | 400 | Invalid hitsPerPage | `"Validation error: hitsPerPage must be between 1 and 1000"` |
| API Error | 500 | API unavailable | `"API error: Failed to fetch from HackerNews API"` |
| Rate Limit | 429 | Too many requests | `"Rate limit exceeded: 10,000 requests per hour maximum"` |
| Network | 503 | Network failure | `"Network error: Could not connect to HackerNews API"` |
**Usage Examples**:
1. **Search for AI stories**:
```typescript
{
query: "AI",
tags: ["story"]
}
```
2. **Find highly-rated recent posts**:
```typescript
{
query: "machine learning",
numericFilters: ["points>=100", "created_at_i>1728000000"],
hitsPerPage: 50
}
```
3. **Paginate through results**:
```typescript
{
query: "Python",
page: 2,
hitsPerPage: 20
}
```
---
### 2. get-front-page
**Purpose**: Retrieve current HackerNews front page posts.
**MCP Registration**:
```typescript
server.registerTool(
'get-front-page',
{
title: 'Get HackerNews Front Page',
description: 'Retrieve posts currently on the HackerNews front page. Returns stories sorted by HackerNews ranking algorithm. Supports pagination to view beyond first page.',
inputSchema: {
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 (1-1000)')
},
outputSchema: {
hits: z.array(z.any()),
nbHits: z.number(),
page: z.number(),
nbPages: z.number(),
hitsPerPage: z.number(),
processingTimeMS: z.number(),
query: z.string(),
params: z.string()
}
},
async (params) => { /* implementation */ }
);
```
**Input Parameters**:
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `page` | `number` | No | `0` | Page number (0-indexed) |
| `hitsPerPage` | `number` | No | `30` | Results per page (1-1000) |
**Output Format**: Same as `SearchResult` type
**Implementation Note**: Internally calls search API with `tags=front_page` filter.
**Success Example**:
```json
{
"hits": [
{
"objectID": "123456",
"title": "Show HN: My Weekend Project",
"url": "https://example.com",
"points": 456,
"num_comments": 123,
"_tags": ["story", "show_hn", "front_page"]
}
],
"nbHits": 30,
"page": 0,
"nbPages": 1,
"hitsPerPage": 30
}
```
**Error Conditions**: Same validation errors as `search-posts` for pagination parameters.
---
### 3. get-latest-posts
**Purpose**: Retrieve most recent HackerNews posts sorted by date.
**MCP Registration**:
```typescript
server.registerTool(
'get-latest-posts',
{
title: 'Get Latest HackerNews Posts',
description: 'Retrieve the most recent posts on HackerNews sorted by creation date (newest first). Optionally filter by type (stories, comments, etc.). Useful for monitoring new content and discussions.',
inputSchema: {
tags: z.array(z.string()).optional().describe('Optional filter tags. Examples: ["story"], ["comment"], ["poll"]'),
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 (1-1000)')
},
outputSchema: {
hits: z.array(z.any()),
nbHits: z.number(),
page: z.number(),
nbPages: z.number(),
hitsPerPage: z.number(),
processingTimeMS: z.number(),
query: z.string(),
params: z.string()
}
},
async (params) => { /* implementation */ }
);
```
**Input Parameters**:
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `tags` | `string[]` | No | `undefined` | Filter by type (story, comment, etc.) |
| `page` | `number` | No | `0` | Page number (0-indexed) |
| `hitsPerPage` | `number` | No | `20` | Results per page (1-1000) |
**Output Format**: Same as `SearchResult` type
**Implementation Note**: Uses `search_by_date` endpoint with empty query.
**Success Example**:
```json
{
"hits": [
{
"objectID": "123457",
"created_at": "2025-10-12T14:25:00.000Z",
"created_at_i": 1728746700,
"title": "Just launched: New Framework",
"author": "developer123",
"points": 5,
"num_comments": 2,
"_tags": ["story"]
}
],
"nbHits": 50000,
"page": 0,
"nbPages": 2500
}
```
**Usage Examples**:
1. **Latest stories only**:
```typescript
{
tags: ["story"]
}
```
2. **Latest comments**:
```typescript
{
tags: ["comment"],
hitsPerPage: 50
}
```
3. **All recent content**:
```typescript
{
page: 0,
hitsPerPage: 100
}
```
---
### 4. get-item
**Purpose**: Retrieve complete details for a specific item including nested comments.
**MCP Registration**:
```typescript
server.registerTool(
'get-item',
{
title: 'Get HackerNews Item',
description: 'Retrieve complete details for a specific HackerNews item (story, comment, poll) by its ID. Returns full nested comment tree for stories. The API returns numeric IDs which are converted to strings for consistency.',
inputSchema: {
itemId: z.string().min(1).describe('HackerNews item ID (e.g., "123456")')
},
outputSchema: {
id: z.string(),
created_at: z.string(),
created_at_i: z.number(),
type: z.enum(['story', 'comment', 'poll', 'pollopt']),
author: z.string(),
title: z.string().nullable(),
url: z.string().nullable(),
text: z.string().nullable(),
points: z.number().nullable(),
parent_id: z.number().nullable(),
story_id: z.number(),
options: z.array(z.number()),
children: z.array(z.any())
}
},
async (params) => { /* implementation */ }
);
```
**Input Parameters**:
| Parameter | Type | Required | Description | Examples |
|-----------|------|----------|-------------|----------|
| `itemId` | `string` | Yes | HackerNews item ID | `"123456"`, `"38456789"` |
**Output Format**:
```typescript
{
id: string, // Converted from number
created_at: string,
created_at_i: number,
type: 'story' | 'comment' | 'poll' | 'pollopt',
author: string,
title: string | null,
url: string | null,
text: string | null,
points: number | null,
parent_id: number | null,
story_id: number, // Always present
options: number[], // Empty array except for polls
children: ItemResult[] // Nested recursively
}
```
**Success Example (Story with Comments)**:
```json
{
"id": "1",
"created_at": "2006-10-09T18:21:51.000Z",
"created_at_i": 1160418111,
"type": "story",
"author": "pg",
"title": "Y Combinator",
"url": "http://ycombinator.com",
"text": null,
"points": 57,
"parent_id": null,
"story_id": 1,
"options": [],
"children": [
{
"id": "15",
"type": "comment",
"author": "sama",
"text": "Great post!",
"points": null,
"parent_id": 1,
"story_id": 1,
"options": [],
"children": []
}
]
}
```
**Error Conditions**:
| Error Type | Condition | Example Message |
|------------|-----------|-----------------|
| Validation | Empty itemId | `"Validation error: itemId is required"` |
| Not Found | Item doesn't exist | `"Item not found: No item with ID '999999'"` |
| API Error | API failure | `"API error: Failed to fetch item details"` |
**Usage Notes**:
- Returns complete nested comment tree (can be deeply nested)
- The API returns `id` as a number, but it's converted to string by the client
- `story_id` and `options` fields are always present (per actual API response)
- No depth limit on children
- Deleted items may return null or error
- Processing time increases with comment count
---
### 5. get-user
**Purpose**: Retrieve user profile information.
**MCP Registration**:
```typescript
server.registerTool(
'get-user',
{
title: 'Get HackerNews User Profile',
description: 'Retrieve public profile information for a HackerNews user by username. Returns karma score and optional bio. Note: The HackerNews API does not return account creation dates.',
inputSchema: {
username: z.string().min(1).regex(/^[a-zA-Z0-9_]+$/).describe('HackerNews username (alphanumeric and underscore only)')
},
outputSchema: {
username: z.string(),
karma: z.number(),
about: z.string().nullable().optional()
}
},
async (params) => { /* implementation */ }
);
```
**Input Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `username` | `string` | Yes | HackerNews username | Alphanumeric + underscore, min 1 char |
**Output Format**:
```typescript
{
username: string,
karma: number,
about?: string | null // Optional bio text
}
```
**Success Example**:
```json
{
"username": "pg",
"karma": 157316,
"about": "Bug fixer."
}
```
**Error Conditions**:
| Error Type | Condition | Example Message |
|------------|-----------|-----------------|
| Validation | Invalid username format | `"Validation error: username must be alphanumeric"` |
| Validation | Empty username | `"Validation error: username is required"` |
| Not Found | User doesn't exist | `"User not found: No user with username 'nonexistent'"` |
| API Error | API failure | `"API error: Failed to fetch user profile"` |
**Usage Examples**:
1. **Get famous user**:
```typescript
{
username: "pg"
}
```
2. **Check author karma**:
```typescript
{
username: "developer123"
}
```
---
## Common Patterns
### Error Response Format
All tools return errors in this format:
```typescript
{
content: [{
type: 'text',
text: JSON.stringify({
type: 'validation' | 'api' | 'network' | 'not_found' | 'rate_limit',
message: string,
details?: any
})
}],
isError: true
}
```
### Success Response Format
All tools return successes in this format:
```typescript
{
content: [{
type: 'text',
text: JSON.stringify(result)
}],
structuredContent: result,
isError: false
}
```
### Tag Values
Common tag values used in HackerNews API:
| Tag | Description | Usage |
|-----|-------------|-------|
| `story` | Story posts | Filter for stories only |
| `comment` | Comments | Filter for comments only |
| `poll` | Polls | Filter for polls only |
| `pollopt` | Poll options | Rarely used directly |
| `show_hn` | Show HN posts | Filter for Show HN |
| `ask_hn` | Ask HN posts | Filter for Ask HN |
| `front_page` | Front page items | Used by get-front-page |
| `author_USERNAME` | By specific author | Filter by author |
| `story_ID` | Comments on story | Filter comments by story |
### Numeric Filter Syntax
Format: `field operator value`
**Supported Fields**:
- `created_at_i`: Unix timestamp
- `points`: Upvote count
- `num_comments`: Comment count
**Supported Operators**:
- `<`: Less than
- `<=`: Less than or equal
- `=`: Equal
- `>=`: Greater than or equal
- `>`: Greater than
**Examples**:
- `points>=100`: At least 100 points
- `created_at_i>1728000000`: After specific date
- `num_comments>=50`: At least 50 comments
---
## API Endpoints Mapping
| Tool | HTTP Method | API Endpoint | Query Params |
|------|-------------|--------------|--------------|
| `search-posts` | GET | `/api/v1/search` | `query`, `tags`, `numericFilters`, `page`, `hitsPerPage` |
| `get-front-page` | GET | `/api/v1/search` | `tags=front_page`, `page`, `hitsPerPage` |
| `get-latest-posts` | GET | `/api/v1/search_by_date` | `tags`, `page`, `hitsPerPage` |
| `get-item` | GET | `/api/v1/items/:id` | None (ID in path) |
| `get-user` | GET | `/api/v1/users/:username` | None (username in path) |
---
## Rate Limiting
All tools are subject to HackerNews API rate limits:
- **Limit**: 10,000 requests per hour per IP address
- **Detection**: 429 HTTP status code from API
- **Response**: Clear error message with rate limit information
- **Retry Strategy**: Client responsibility (not handled by server)
**Rate Limit Error Example**:
```json
{
"type": "rate_limit",
"message": "Rate limit exceeded: 10,000 requests per hour maximum. Please try again later.",
"statusCode": 429
}
```
---
## Testing Requirements
### Contract Tests
Each tool MUST have contract tests verifying:
1. **Input Validation**: All validation rules enforced
2. **Output Schema**: Response matches defined schema
3. **Error Handling**: All error conditions handled correctly
4. **Edge Cases**: Boundary values, empty results, etc.
### Integration Tests
Each tool MUST have integration tests with real API:
1. **Success Cases**: Valid inputs return expected results
2. **API Errors**: Handle API failures gracefully
3. **Network Errors**: Handle connection failures
4. **Rate Limits**: Handle 429 responses correctly
### Example Test Structure
```typescript
describe('search-posts tool', () => {
describe('input validation', () => {
it('should reject empty query', async () => {
// Test validation error
});
it('should accept valid query', async () => {
// Test valid input
});
});
describe('API integration', () => {
it('should return search results for valid query', async () => {
// Test with real API
});
it('should handle API errors', async () => {
// Test error handling
});
});
});
```
---
## Version Compatibility
**Current Version**: 1.0.0
**Breaking Changes Policy**:
- Tool names MUST NOT change (breaking)
- Required parameters MUST NOT be added (breaking)
- Output schema MUST NOT remove fields (breaking)
- Optional parameters MAY be added (non-breaking)
- Output schema MAY add optional fields (non-breaking)
**Deprecation Process**:
1. Mark tool/parameter as deprecated in documentation
2. Add deprecation warning in tool description
3. Support for minimum 6 months
4. Remove in next major version
---
## Summary
**Total Tools**: 5 defined
**Tool Categories**:
- **Search**: `search-posts`, `get-latest-posts`
- **Discovery**: `get-front-page`
- **Details**: `get-item`, `get-user`
**Input Validation**: Zod schemas for all inputs
**Output Format**: Consistent JSON structure with structured content
**Error Handling**: Standardized error responses with clear messages
**Testing**: 100% contract and integration test coverage required
**Next Phase**: Create quickstart guide and update agent context