=== NEW SERVERS ADDED (7) === - servers/closebot — 119 tools, 14 modules, 4,656 lines TS (Stage 7) - servers/google-console — Google Search Console MCP (Stage 7) - servers/meta-ads — Meta/Facebook Ads MCP (Stage 8) - servers/twilio — Twilio communications MCP (Stage 8) - servers/competitor-research — Competitive intel MCP (Stage 6) - servers/n8n-apps — n8n workflow MCP apps (Stage 6) - servers/reonomy — Commercial real estate MCP (Stage 1) === FACTORY INFRASTRUCTURE ADDED === - infra/factory-tools — mcp-jest, mcp-validator, mcp-add, MCP Inspector - 60 test configs, 702 auto-generated test cases - All 30 servers score 100/100 protocol compliance - infra/command-center — Pipeline state, operator playbook, dashboard config - infra/factory-reviews — Automated eval reports === DOCS ADDED === - docs/MCP-FACTORY.md — Factory overview - docs/reports/ — 5 pipeline evaluation reports - docs/research/ — Browser MCP research === RULES ESTABLISHED === - CONTRIBUTING.md — All MCP work MUST go in this repo - README.md — Full inventory of 37 servers + infra docs - .gitignore — Updated for Python venvs TOTAL: 37 MCP servers + full factory pipeline in one repo. This is now the single source of truth for all MCP work.
168 lines
4.5 KiB
TypeScript
168 lines
4.5 KiB
TypeScript
/**
|
|
* Token bucket rate limiter for Google Search Console API quotas
|
|
* Implements per-API-type tracking with automatic token refill
|
|
*/
|
|
|
|
export type APIType = 'gsc' | 'inspection' | 'indexing';
|
|
|
|
interface BucketConfig {
|
|
capacity: number;
|
|
refillRate: number; // tokens per millisecond
|
|
refillInterval: number; // interval in ms
|
|
}
|
|
|
|
interface TokenBucket {
|
|
tokens: number;
|
|
lastRefill: number;
|
|
config: BucketConfig;
|
|
queue: Array<{ resolve: () => void; tokens: number }>;
|
|
}
|
|
|
|
export interface RateLimitConfig {
|
|
/** GSC API: 1200 queries/minute (default) */
|
|
gscQueries?: number;
|
|
/** URL Inspection: 600/day per property, 2000/day total */
|
|
inspectionQueries?: number;
|
|
/** Indexing API: 200/day */
|
|
indexingQueries?: number;
|
|
}
|
|
|
|
export class RateLimiter {
|
|
private buckets = new Map<APIType, TokenBucket>();
|
|
|
|
constructor(config: RateLimitConfig = {}) {
|
|
// GSC API: 1200 queries/minute
|
|
const gscQueriesPerMin = config.gscQueries ?? 1200;
|
|
this.buckets.set('gsc', {
|
|
tokens: gscQueriesPerMin,
|
|
lastRefill: Date.now(),
|
|
config: {
|
|
capacity: gscQueriesPerMin,
|
|
refillRate: gscQueriesPerMin / (60 * 1000), // per ms
|
|
refillInterval: 60 * 1000 // 1 minute
|
|
},
|
|
queue: []
|
|
});
|
|
|
|
// URL Inspection: 600/day (simplified - not tracking per-property)
|
|
const inspectionQueriesPerDay = config.inspectionQueries ?? 600;
|
|
this.buckets.set('inspection', {
|
|
tokens: inspectionQueriesPerDay,
|
|
lastRefill: Date.now(),
|
|
config: {
|
|
capacity: inspectionQueriesPerDay,
|
|
refillRate: inspectionQueriesPerDay / (24 * 60 * 60 * 1000), // per ms
|
|
refillInterval: 24 * 60 * 60 * 1000 // 1 day
|
|
},
|
|
queue: []
|
|
});
|
|
|
|
// Indexing API: 200/day
|
|
const indexingQueriesPerDay = config.indexingQueries ?? 200;
|
|
this.buckets.set('indexing', {
|
|
tokens: indexingQueriesPerDay,
|
|
lastRefill: Date.now(),
|
|
config: {
|
|
capacity: indexingQueriesPerDay,
|
|
refillRate: indexingQueriesPerDay / (24 * 60 * 60 * 1000), // per ms
|
|
refillInterval: 24 * 60 * 60 * 1000 // 1 day
|
|
},
|
|
queue: []
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Refill tokens in a bucket based on elapsed time
|
|
*/
|
|
private refillBucket(bucket: TokenBucket): void {
|
|
const now = Date.now();
|
|
const elapsed = now - bucket.lastRefill;
|
|
const tokensToAdd = elapsed * bucket.config.refillRate;
|
|
|
|
bucket.tokens = Math.min(
|
|
bucket.config.capacity,
|
|
bucket.tokens + tokensToAdd
|
|
);
|
|
bucket.lastRefill = now;
|
|
}
|
|
|
|
/**
|
|
* Process queued requests if tokens available
|
|
*/
|
|
private processQueue(apiType: APIType): void {
|
|
const bucket = this.buckets.get(apiType);
|
|
if (!bucket) return;
|
|
|
|
this.refillBucket(bucket);
|
|
|
|
while (bucket.queue.length > 0 && bucket.tokens >= bucket.queue[0].tokens) {
|
|
const item = bucket.queue.shift()!;
|
|
bucket.tokens -= item.tokens;
|
|
item.resolve();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Acquire tokens for an API call
|
|
* Waits if insufficient tokens are available
|
|
*/
|
|
async acquire(apiType: APIType, tokens: number = 1): Promise<void> {
|
|
const bucket = this.buckets.get(apiType);
|
|
if (!bucket) {
|
|
throw new Error(`Unknown API type: ${apiType}`);
|
|
}
|
|
|
|
this.refillBucket(bucket);
|
|
|
|
// If enough tokens available, consume immediately
|
|
if (bucket.tokens >= tokens) {
|
|
bucket.tokens -= tokens;
|
|
return;
|
|
}
|
|
|
|
// Otherwise, queue the request
|
|
return new Promise<void>((resolve) => {
|
|
bucket.queue.push({ resolve, tokens });
|
|
|
|
// Set up periodic processing
|
|
const interval = setInterval(() => {
|
|
this.processQueue(apiType);
|
|
|
|
// Stop if this request was processed
|
|
if (!bucket.queue.find(item => item.resolve === resolve)) {
|
|
clearInterval(interval);
|
|
}
|
|
}, 100); // Check every 100ms
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get current token counts for monitoring
|
|
*/
|
|
getStatus(): Record<APIType, { tokens: number; capacity: number; queued: number }> {
|
|
const status = {} as Record<APIType, { tokens: number; capacity: number; queued: number }>;
|
|
|
|
for (const [apiType, bucket] of this.buckets.entries()) {
|
|
this.refillBucket(bucket);
|
|
status[apiType] = {
|
|
tokens: Math.floor(bucket.tokens),
|
|
capacity: bucket.config.capacity,
|
|
queued: bucket.queue.length
|
|
};
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
/**
|
|
* Reset all buckets (useful for testing)
|
|
*/
|
|
reset(): void {
|
|
for (const bucket of this.buckets.values()) {
|
|
bucket.tokens = bucket.config.capacity;
|
|
bucket.lastRefill = Date.now();
|
|
bucket.queue = [];
|
|
}
|
|
}
|
|
}
|