Architecture Overview
IPKit is a Node.js server that exposes intellectual property search and analysis capabilities via the Model Context Protocol (MCP). It normalizes data from 10 different IP offices into consistent schemas, handles authentication, rate limiting, caching, and error recovery for each provider.
System Diagram
Section titled “System Diagram”┌─────────────────────────────────────────────────────────────┐│ AI Clients ││ Claude Desktop │ ChatGPT │ HTTP API │ Other MCP │└────────┬─────────┴─────┬─────┴──────┬─────┴────────────────┘ │ stdio │ SSE │ HTTP ▼ ▼ ▼┌─────────────────────────────────────────────────────────────┐│ Transport Layer ││ StdioServerTransport │ SSEServerTransport │ StreamableHTTP ││ │ (ChatGPT) │ (Hosted) │└────────────────────────────┬────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────┐│ MCP Server (server.ts) ││ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ ││ │ Tool Registry│ │ Dispatcher │ │ Analytics │ ││ │ (declarative)│ │ (validation │ │ (per-tool stats, │ ││ │ │ │ + execution │ │ latency P50/95) │ ││ │ │ │ + errors) │ │ │ ││ └──────────────┘ └──────┬───────┘ └──────────────────┘ │└────────────────────────────┬────────────────────────────────┘ │ ┌──────────────┼──────────────┐ ▼ ▼ ▼┌──────────────────┐ ┌────────────┐ ┌────────────────┐│ Tool Layer │ │ Cache │ │ Service ││ (trademarkSearch,│ │ (memory or │ │ Container ││ clearance, │ │ file) │ │ (services.ts) ││ designSearch, │ │ │ │ ││ patentSearch, │ │ TTLs: │ │ Lazy singletons││ monitoring...) │ │ search: 5m │ │ Three-state: ││ │ │ status: 1h │ │ undefined→null ││ │ │ class: 24h │ │ →instance │└────────┬─────────┘ └────────────┘ └────────┬───────┘ │ │ ▼ ▼┌─────────────────────────────────────────────────────────────┐│ Provider Layer ││ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ││ │ USPTO │ │ EUIPO │ │IP Aust. │ │ EPO │ ... ││ │(REST/Key)│ │(REST/ │ │(REST/ │ │(REST/ │ ││ │ │ │ OAuth2) │ │ OAuth2) │ │ OAuth2) │ ││ └──────────┘ └──────────┘ └──────────┘ └──────────┘ ││ ││ Each provider: client.ts → transformer.ts → types.ts ││ With: RateLimiter, CircuitBreaker, withRetry() │└─────────────────────────────────────────────────────────────┘Entry Point Flow
Section titled “Entry Point Flow”The server starts in src/index.ts:
- Configuration —
src/config.tsloads environment variables, validates them with Zod, and exports a typedConfigobject. - Transport selection — Based on
config.transport, the entry point invokes one of three startup functions:runStdioServer()for local Claude Desktop usagerunHttpServer(config)for hosted or self-hosted HTTP deploymentrunChatGptServer(config)for ChatGPT Apps SDK via SSE
- Server creation —
createServer()insrc/server.tsinstantiates an MCPServer, registers all tools, and connects the transport. - Graceful shutdown — Signal handlers (
SIGINT,SIGTERM) calldestroyServices()to clean up cache intervals, analytics emitters, and active sessions.
Tool Registration and Dispatch
Section titled “Tool Registration and Dispatch”Tools are registered declaratively in a toolRegistry map inside server.ts. Each entry binds a tool name, Zod input schema, description, and executor function.
The centralized dispatcher handles all tool calls:
- Validation — Input arguments are parsed against the tool’s Zod schema. Invalid input returns a structured
INVALID_INPUTerror. - Execution — The tool’s executor function is called with validated arguments.
- Analytics — A
ToolCallEventis recorded in theAnalyticscollector (in afinallyblock) with tool name, duration, success/failure, cache hit, and error details. - Error formatting — If the executor throws a
TrademarkError, it is converted to an MCP error response viatoMcpError(). Unexpected errors are wrapped asINTERNAL_ERROR.
Adding a new tool requires a single registerTool() call — no routing or middleware changes needed.
Provider Pattern
Section titled “Provider Pattern”TrademarkProvider Interface
Section titled “TrademarkProvider Interface”Every trademark jurisdiction implements the TrademarkProvider interface:
interface TrademarkProvider { jurisdiction: Jurisdiction; isConfigured(): boolean; search(params: SearchParams): Promise<SearchResult>; getDetails(identifier: string): Promise<TrademarkDetail>; getStatus(identifier: string, options?): Promise<StatusResponse>;}ProviderRegistry
Section titled “ProviderRegistry”The ProviderRegistry is a Map<Jurisdiction, TrademarkProvider> that stores all registered providers. Tools call registry.getConfigured() to get the list of providers that have valid credentials, and registry.get(jurisdiction) to retrieve a specific provider.
Provider File Structure
Section titled “Provider File Structure”Each provider lives in src/providers/{jurisdiction}/ with three files:
types.ts— TypeScript interfaces for the raw API response shapesclient.ts— ImplementsTrademarkProviderwith rate limiting, retries, and error handlingtransformer.ts— Normalizes API-specific response shapes intoTrademarkSummaryandTrademarkDetail
Design and patent providers follow the same pattern but do not implement TrademarkProvider (they are separate IP domains with different schemas, classifications, and lifecycles).
Resilience
Section titled “Resilience”Each provider client wraps API calls with:
RateLimiter— Token bucket that enforces per-provider request limits (configurable via env vars)withRetry()— Automatic retry with exponential backoff for transient errors (5xx, timeouts)CircuitBreaker— Shared circuit breakers for providers with common OAuth infrastructure (EUIPO, IP Australia, EPO). Opens after repeated failures to avoid cascading timeouts.TrademarkError— All errors are wrapped with error codes, provider name, and retry metadata
Service Container
Section titled “Service Container”src/services.ts is the shared service container. It provides lazy singletons for:
- Provider registry
- Search and status caches
- EUIPO sub-clients (G&S, Designs, Persons)
- IP Australia sub-clients (Designs, Patents)
- EPO client
- Analytics collector and emitter
- Trademark watcher and webhook dispatcher
- API key store, quota tracker, rate limiter
Three-State Singleton Pattern
Section titled “Three-State Singleton Pattern”Optional services (like EUIPO sub-clients) use a three-state pattern:
let _euipoGSClient: EUIPOGSClient | null | undefined = undefined;
export function getEUIPOGSClient(): EUIPOGSClient | null { if (_euipoGSClient === undefined) { // unchecked const client = new EUIPOGSClient(); _euipoGSClient = client.isConfigured() ? client // ready : null; // not configured } return _euipoGSClient;}undefined— not yet checkednull— checked, not configured (credentials missing or provider disabled)- instance — checked, configured, ready to use
Tools check for null and return a clear error message like “EUIPO provider is not configured” instead of throwing.
Caching
Section titled “Caching”IPKit uses an in-memory or file-based cache with configurable TTLs:
| Cache | Default TTL | Purpose |
|---|---|---|
| Search results | 5 minutes | Avoid re-querying upstream APIs for identical searches |
| Status lookups | 1 hour | Trademark status changes infrequently |
| Nice class / G&S | 24 hours | Classification data is essentially static |
The cache backend is selected by configuration:
CACHE_DIRunset —MemoryCache(volatile, lost on restart)CACHE_DIRset —FileCache(persists to disk as JSON)
Both backends implement the same Cache interface and support getStats() for analytics integration.
Error Handling
Section titled “Error Handling”All errors flow through the TrademarkError class:
throw new TrademarkError({ code: ErrorCode.PROVIDER_ERROR, message: 'EUIPO service error (HTTP 500)', provider: 'EUIPO', retryable: true, retryAfterMs: 5000,});The handleProviderError() utility maps HTTP status codes to appropriate error codes:
- 401/403 ->
AUTH_FAILED - 404 ->
TRADEMARK_NOT_FOUND - 429 ->
RATE_LIMITED(withretryAfterMsfromRetry-Afterheader) - 5xx ->
PROVIDER_ERROR - Timeouts ->
PROVIDER_TIMEOUT - DNS/connection errors ->
PROVIDER_UNAVAILABLE
Multi-Jurisdiction Graceful Degradation
Section titled “Multi-Jurisdiction Graceful Degradation”When searching across multiple jurisdictions, individual provider failures do not abort the entire search. IPKit uses Promise.allSettled() to collect results from all providers, returns data from successful providers, and reports per-jurisdiction errors in the response metadata.
OAuth Pre-warming
Section titled “OAuth Pre-warming”On HTTP server startup, prewarmOAuth() eagerly fetches OAuth tokens for configured providers (IP Australia, EUIPO, EPO) in a fire-and-forget pattern. This avoids cold-start latency on the first user request after deployment. If pre-warming fails, the normal lazy initialization path still works.