relentless_read
Retrieve specific entries from Notion databases using slugs to access complete content and properties for content management workflows.
Instructions
Read a specific entry from a Notion database by its slug. Returns the full entry with all properties and content.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| database | Yes | The database name (e.g., "blog", "docs", "leads") | |
| slug | Yes | The slug of the entry to read (e.g., "getting-started", "hello-world") |
Implementation Reference
- src/index.ts:429-452 (handler)The core handler function for the 'relentless_read' tool. Validates input parameters (database and slug), constructs the Relentless API endpoint, fetches the entry data using the shared relentlessRequest helper, and returns the full JSON response as text content.case 'relentless_read': { const { database, slug } = args as { database: string; slug: string } if (!database || !slug) { throw new McpError( ErrorCode.InvalidParams, 'Missing required parameters: database and slug' ) } console.error(`[${new Date().toISOString()}] Reading ${database}/${slug}`) const endpoint = `${RELENTLESS_API_BASE}/api/v1/public/db/${database}/read/${slug}` const result = await relentlessRequest(endpoint) return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], } }
- src/index.ts:244-263 (registration)Tool registration in the ListTools response, defining the name, description, and input schema (parameters: database and slug). This makes the tool discoverable by MCP clients.{ name: 'relentless_read', description: 'Read a specific entry from a Notion database by its slug. Returns the full entry with all properties and content.', inputSchema: { type: 'object', properties: { database: { type: 'string', description: 'The database name (e.g., "blog", "docs", "leads")', }, slug: { type: 'string', description: 'The slug of the entry to read (e.g., "getting-started", "hello-world")', }, }, required: ['database', 'slug'], }, },
- src/index.ts:64-171 (helper)Shared helper function used by relentless_read (and other tools) to make API requests to Relentless CMS with robust retry logic, rate limiting handling, timeouts, and detailed error messages.async function relentlessRequest( endpoint: string, options?: RequestInit, maxRetries = 3 ): Promise<any> { const url = endpoint.includes('?') ? `${endpoint}&api_key=${RELENTLESS_API_KEY}` : `${endpoint}?api_key=${RELENTLESS_API_KEY}` let lastError: Error | null = null for (let attempt = 0; attempt < maxRetries; attempt++) { try { // Add timeout using AbortController const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), 30000) // 30s timeout const response = await fetch(url, { ...options, signal: controller.signal, headers: { 'Content-Type': 'application/json', ...options?.headers, }, }) clearTimeout(timeoutId) // Handle rate limiting (429) if (response.status === 429) { const retryAfter = response.headers.get('Retry-After') const delay = retryAfter ? parseInt(retryAfter) * 1000 : Math.pow(2, attempt) * 1000 console.error(`⏳ Rate limited. Retrying in ${delay / 1000}s...`) await sleep(delay) continue } // Handle success if (response.ok) { return response.json() } // Handle errors with detailed messages const errorBody = await response.json().catch(() => ({ message: 'Unknown error' })) let errorMessage = errorBody.message || errorBody.error || 'Unknown error' // Provide helpful error messages based on status code switch (response.status) { case 401: errorMessage = 'Invalid API key. Check RELENTLESS_API_KEY environment variable.' break case 403: errorMessage = 'Access forbidden. Verify you own this API and have proper permissions.' break case 404: errorMessage = `API path not found. Check that the API exists in your Relentless dashboard.` break case 422: errorMessage = `Validation error: ${errorMessage}. Check that property names match your Notion database exactly (case-sensitive).` break case 500: errorMessage = `Relentless API error: ${errorMessage}` break } // Don't retry client errors (4xx) except 429 if (response.status >= 400 && response.status < 500) { throw new McpError( ErrorCode.InvalidRequest, `Relentless API error (${response.status}): ${errorMessage}` ) } // Server errors (5xx) - will retry lastError = new Error(`Server error (${response.status}): ${errorMessage}`) console.error(`❌ Attempt ${attempt + 1}/${maxRetries} failed: ${errorMessage}`) } catch (error: any) { lastError = error // Handle timeout if (error.name === 'AbortError') { throw new McpError(ErrorCode.InternalError, 'Request timeout after 30 seconds') } // Handle MCP errors (don't retry) if (error instanceof McpError) { throw error } // Network errors - will retry console.error(`❌ Attempt ${attempt + 1}/${maxRetries} failed: ${error.message}`) } // Exponential backoff before retry (except on last attempt) if (attempt < maxRetries - 1) { const backoffDelay = Math.min(Math.pow(2, attempt) * 1000, 10000) // Max 10s console.error(`⏳ Retrying in ${backoffDelay / 1000}s...`) await sleep(backoffDelay) } } // All retries exhausted throw new McpError( ErrorCode.InternalError, `Failed after ${maxRetries} attempts: ${lastError?.message || 'Unknown error'}` ) }