ation endpoint designed specifically for machine consumption. The service exposes a REST interface and a Model Context Protocol (MCP) server, both requiring zero configuration.
Implementation Strategy
We recommend wrapping the endpoint in a typed utility that handles URL encoding, error states, and response validation. This ensures robustness in production environments.
TypeScript Metadata Resolver
import { z } from 'zod';
// Define strict schema for response validation
const LinkMetadataSchema = z.object({
url: z.string().url(),
resolvedUrl: z.string().url(),
title: z.string().optional(),
description: z.string().optional(),
image: z.string().url().optional(),
siteName: z.string().optional(),
favicon: z.string().url().optional(),
type: z.string().optional(),
oembed: z.any().optional(),
fetchedAt: z.string().datetime(),
});
export type LinkMetadata = z.infer<typeof LinkMetadataSchema>;
const ENDPOINT_BASE = 'https://openunfurl.vercel.app/api/unfurl';
export class MetadataResolver {
private readonly timeoutMs: number;
constructor(timeoutMs = 5000) {
this.timeoutMs = timeoutMs;
}
async resolve(targetUrl: string): Promise<LinkMetadata> {
const params = new URLSearchParams({ url: targetUrl });
const requestUrl = `${ENDPOINT_BASE}?${params.toString()}`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
try {
const response = await fetch(requestUrl, {
signal: controller.signal,
headers: { 'Accept': 'application/json' },
});
if (!response.ok) {
throw new Error(`Metadata fetch failed: ${response.status} ${response.statusText}`);
}
const rawData = await response.json();
return LinkMetadataSchema.parse(rawData);
} finally {
clearTimeout(timeoutId);
}
}
}
Usage Example
const resolver = new MetadataResolver(3000);
try {
const meta = await resolver.resolve('https://github.com');
console.log(`Title: ${meta.title}`);
console.log(`Resolved: ${meta.resolvedUrl}`);
console.log(`Fetched: ${meta.fetchedAt}`);
} catch (error) {
console.error('Resolution failed:', error);
}
Key Architecture Decisions:
- URLSearchParams for Safety: Using
URLSearchParams instead of template literals prevents injection vulnerabilities and ensures proper encoding of complex URLs.
- Schema Validation: Zod validation guarantees that the response conforms to expectations, preventing runtime errors from malformed data.
- Timeout Control: Explicit timeout handling prevents hanging requests from blocking agent workflows.
- Resolved URL Tracking: The service returns both the input URL and the
resolvedUrl, allowing agents to detect redirects and canonicalize links.
MCP Server Integration
For agents using the Model Context Protocol, the service provides a remote MCP endpoint. This uses Streamable HTTP, the current standard transport, which is stateless and compatible with scale-to-zero serverless architectures.
MCP Configuration
{
"mcpServers": {
"metadata-extractor": {
"url": "https://openunfurl.vercel.app/api/mcp"
}
}
}
Tool Invocation
The MCP server exposes a single tool named unfurl. Agents can invoke it via JSON-RPC 2.0:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "unfurl",
"arguments": {
"url": "https://example.com"
}
}
}
This integration allows agents to resolve metadata natively without custom HTTP logic, fitting seamlessly into agentic tool-calling loops.
Pitfall Guide
-
Client-Side Rendering Blind Spot
- Explanation: The service parses static HTML returned by the server. Single Page Applications (SPAs) that render metadata via JavaScript will return empty or incomplete fields.
- Fix: Accept this limitation for static content. For SPA-heavy targets, implement a fallback to a headless browser service or skip metadata extraction for known SPA domains.
-
SSRF Protection Triggers
- Explanation: The endpoint enforces SSRF guards and rejects requests targeting internal or private IP addresses.
- Fix: Validate URLs client-side before calling the service. Ensure targets are public-facing domains. Attempting to resolve internal URLs will result in errors.
-
Rate Limiting Assumptions
- Explanation: Rate limiting is best-effort and per-instance. There is no guaranteed quota or contractual SLA.
- Fix: Implement exponential backoff and retry logic. Avoid firehosing the endpoint with high-concurrency requests. For enterprise-scale needs, consider a self-hosted solution or paid alternative.
-
Transport Version Mismatch
- Explanation: Older MCP clients may attempt to use HTTP+SSE, which was deprecated in mid-2025. The service uses Streamable HTTP.
- Fix: Ensure MCP client libraries are updated to support Streamable HTTP. Verify transport compatibility before deployment.
-
Stale Metadata Cache
- Explanation: The response includes a
fetchedAt timestamp. Metadata may become outdated if the source page changes.
- Fix: Check the
fetchedAt field against your freshness requirements. Implement cache invalidation logic based on timestamp thresholds.
-
Ignoring Redirects
- Explanation: The service resolves redirects and returns the final URL. Ignoring
resolvedUrl can lead to broken links or mismatched metadata.
- Fix: Always use
resolvedUrl for downstream processing. Update stored URLs to reflect the canonical destination.
-
Missing Favicon Handling
- Explanation: The
favicon field may be null if the service cannot locate an icon.
- Fix: Handle null favicon values gracefully in UI components. Provide a default placeholder image when
favicon is absent.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Autonomous Agent Workflow | Authless Unfurl Service | No signup required; enables full agent autonomy | Free |
| High-Volume RAG Ingestion | Authless Unfurl Service | Low token cost; structured output reduces LLM load | Free |
| SPA-Heavy Target Sites | Headless Browser Proxy | Requires JS execution for metadata extraction | High (Compute) |
| Enterprise SLA Requirements | Microlink / OpenGraph.io | Contractual guarantees; dedicated support | Paid |
| Internal Link Resolution | Self-Hosted Parser | SSRF guards block internal IPs; custom solution needed | Dev Effort |
Configuration Template
TypeScript Interface and MCP Config
// metadata.types.ts
export interface LinkMetadata {
url: string;
resolvedUrl: string;
title?: string;
description?: string;
image?: string;
siteName?: string;
favicon?: string;
type?: string;
oembed?: any;
fetchedAt: string;
}
// mcp-config.json
{
"mcpServers": {
"openunfurl": {
"url": "https://openunfurl.vercel.app/api/mcp"
}
}
}
Quick Start Guide
- Install Dependencies: Ensure your environment supports
fetch and zod for validation.
- Create Resolver: Copy the
MetadataResolver class into your codebase.
- Resolve Metadata: Call
resolver.resolve('https://example.com') to fetch structured data.
- Integrate MCP: Add the MCP configuration to your agent's tool registry for native support.
- Handle Responses: Parse the validated metadata and use fields like
title, description, and image in your application.
This approach provides a robust, cost-effective solution for metadata extraction that respects agent autonomy while minimizing token overhead and operational friction.