=== 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.
460 lines
11 KiB
TypeScript
460 lines
11 KiB
TypeScript
import type { AppDefinition } from './types.js';
|
|
|
|
export const quickWinsApp: AppDefinition = {
|
|
toolName: 'quick_wins_board',
|
|
resourceUri: 'gsc://apps/quick-wins',
|
|
description: 'Interactive board showing optimization opportunities ranked by potential impact',
|
|
annotations: {
|
|
readOnlyHint: true,
|
|
idempotentHint: true,
|
|
},
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
siteUrl: {
|
|
type: 'string',
|
|
description: 'The site URL (must be verified in Google Search Console)',
|
|
},
|
|
startDate: {
|
|
type: 'string',
|
|
description: 'Start date (YYYY-MM-DD)',
|
|
},
|
|
endDate: {
|
|
type: 'string',
|
|
description: 'End date (YYYY-MM-DD)',
|
|
},
|
|
days: {
|
|
type: 'number',
|
|
description: 'Number of days to look back',
|
|
},
|
|
},
|
|
required: ['siteUrl'],
|
|
},
|
|
getHtml: () => `<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Quick Wins Board</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: system-ui, -apple-system, sans-serif;
|
|
background: #f8f9fa;
|
|
color: #202124;
|
|
padding: 16px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.header {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 24px;
|
|
font-weight: 500;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.summary {
|
|
display: flex;
|
|
gap: 24px;
|
|
margin-bottom: 16px;
|
|
font-size: 14px;
|
|
color: #5f6368;
|
|
}
|
|
|
|
.summary-item strong {
|
|
color: #1a73e8;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.filters {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-bottom: 24px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.filter-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.filter-group label {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: #5f6368;
|
|
}
|
|
|
|
select, input {
|
|
padding: 6px 12px;
|
|
border: 1px solid #dadce0;
|
|
border-radius: 4px;
|
|
font-size: 13px;
|
|
background: white;
|
|
color: #202124;
|
|
}
|
|
|
|
select:focus, input:focus {
|
|
outline: none;
|
|
border-color: #1a73e8;
|
|
}
|
|
|
|
.cards-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
gap: 16px;
|
|
}
|
|
|
|
.opportunity-card {
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.08);
|
|
transition: box-shadow 0.2s;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.opportunity-card:hover {
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.15), 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.card-keyword {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #202124;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.card-url {
|
|
font-size: 12px;
|
|
color: #1a73e8;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.card-metrics {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.metric-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.metric-label {
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
color: #5f6368;
|
|
font-weight: 500;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.metric-value {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.position-badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.position-badge.green {
|
|
background: #e6f4ea;
|
|
color: #137333;
|
|
}
|
|
|
|
.position-badge.yellow {
|
|
background: #fef7e0;
|
|
color: #b06000;
|
|
}
|
|
|
|
.position-badge.orange {
|
|
background: #fce8e6;
|
|
color: #c5221f;
|
|
}
|
|
|
|
.card-highlight {
|
|
background: #e8f0fe;
|
|
padding: 12px;
|
|
border-radius: 4px;
|
|
border-left: 3px solid #1a73e8;
|
|
}
|
|
|
|
.card-highlight .label {
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
color: #1967d2;
|
|
font-weight: 600;
|
|
letter-spacing: 0.5px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.card-highlight .value {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: #1a73e8;
|
|
}
|
|
|
|
.impact-bar {
|
|
height: 6px;
|
|
background: #e8eaed;
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.impact-bar-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #34a853, #fbbc04, #ea4335);
|
|
transition: width 0.3s;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 48px;
|
|
color: #5f6368;
|
|
font-size: 14px;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
body {
|
|
background: #1e1e1e;
|
|
color: #e8eaed;
|
|
}
|
|
|
|
select, input {
|
|
background: #2d2d2d;
|
|
color: #e8eaed;
|
|
border-color: #5f6368;
|
|
}
|
|
|
|
.opportunity-card {
|
|
background: #2d2d2d;
|
|
}
|
|
|
|
.card-keyword {
|
|
color: #e8eaed;
|
|
}
|
|
|
|
.card-url {
|
|
color: #8ab4f8;
|
|
}
|
|
|
|
.metric-label {
|
|
color: #9aa0a6;
|
|
}
|
|
|
|
.position-badge.green {
|
|
background: #0d652d;
|
|
color: #81c995;
|
|
}
|
|
|
|
.position-badge.yellow {
|
|
background: #7c4a03;
|
|
color: #fdd663;
|
|
}
|
|
|
|
.position-badge.orange {
|
|
background: #8a1a18;
|
|
color: #f28b82;
|
|
}
|
|
|
|
.card-highlight {
|
|
background: #1a3a52;
|
|
border-left-color: #8ab4f8;
|
|
}
|
|
|
|
.card-highlight .label {
|
|
color: #aecbfa;
|
|
}
|
|
|
|
.card-highlight .value {
|
|
color: #8ab4f8;
|
|
}
|
|
|
|
.impact-bar {
|
|
background: #3c4043;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.cards-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.summary {
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>Quick Wins Board</h1>
|
|
<div class="summary">
|
|
<div class="summary-item">
|
|
<strong id="total-opportunities">0</strong> opportunities
|
|
</div>
|
|
<div class="summary-item">
|
|
Est. <strong id="total-clicks-gain">0</strong> additional clicks/month
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="filters">
|
|
<div class="filter-group">
|
|
<label for="position-filter">Position:</label>
|
|
<select id="position-filter">
|
|
<option value="all">All</option>
|
|
<option value="4-10">4-10</option>
|
|
<option value="11-20">11-20</option>
|
|
</select>
|
|
</div>
|
|
<div class="filter-group">
|
|
<label for="impressions-filter">Min Impressions:</label>
|
|
<input type="number" id="impressions-filter" value="0" min="0" step="100">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="cards-grid" id="cards-container"></div>
|
|
<div class="empty-state" id="empty-state" style="display: none;">
|
|
No opportunities match your filters
|
|
</div>
|
|
|
|
<script>
|
|
const DATA = {{DATA}};
|
|
let filteredData = [...DATA.opportunities];
|
|
|
|
function formatNumber(num) {
|
|
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
|
|
return num.toLocaleString();
|
|
}
|
|
|
|
function formatPercent(num) {
|
|
return (num * 100).toFixed(1) + '%';
|
|
}
|
|
|
|
function getPositionBadgeClass(position) {
|
|
if (position <= 10) return 'green';
|
|
if (position <= 14) return 'yellow';
|
|
return 'orange';
|
|
}
|
|
|
|
function renderCard(opp) {
|
|
const badgeClass = getPositionBadgeClass(opp.position);
|
|
const impactPercent = (opp.impactScore / 100) * 100;
|
|
|
|
return \`
|
|
<div class="opportunity-card">
|
|
<div class="card-keyword">\${opp.keyword}</div>
|
|
<div class="card-url" title="\${opp.page}">\${opp.page}</div>
|
|
|
|
<div class="card-metrics">
|
|
<div class="metric-item">
|
|
<div class="metric-label">Position</div>
|
|
<div class="metric-value">
|
|
<span class="position-badge \${badgeClass}">#\${opp.position.toFixed(1)}</span>
|
|
</div>
|
|
</div>
|
|
<div class="metric-item">
|
|
<div class="metric-label">Impressions</div>
|
|
<div class="metric-value">\${formatNumber(opp.impressions)}</div>
|
|
</div>
|
|
<div class="metric-item">
|
|
<div class="metric-label">Current CTR</div>
|
|
<div class="metric-value">\${formatPercent(opp.currentCtr)}</div>
|
|
</div>
|
|
<div class="metric-item">
|
|
<div class="metric-label">Impact Score</div>
|
|
<div class="metric-value">\${opp.impactScore.toFixed(0)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-highlight">
|
|
<div class="label">Estimated Click Gain</div>
|
|
<div class="value">+\${formatNumber(opp.estimatedClickGain)} clicks/mo</div>
|
|
</div>
|
|
|
|
<div class="impact-bar">
|
|
<div class="impact-bar-fill" style="width: \${impactPercent}%"></div>
|
|
</div>
|
|
</div>
|
|
\`;
|
|
}
|
|
|
|
function renderCards() {
|
|
const container = document.getElementById('cards-container');
|
|
const emptyState = document.getElementById('empty-state');
|
|
|
|
if (filteredData.length === 0) {
|
|
container.innerHTML = '';
|
|
emptyState.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
emptyState.style.display = 'none';
|
|
container.innerHTML = filteredData.map(renderCard).join('');
|
|
}
|
|
|
|
function updateSummary() {
|
|
document.getElementById('total-opportunities').textContent = filteredData.length;
|
|
const totalGain = filteredData.reduce((sum, opp) => sum + opp.estimatedClickGain, 0);
|
|
document.getElementById('total-clicks-gain').textContent = formatNumber(totalGain);
|
|
}
|
|
|
|
function applyFilters() {
|
|
const positionFilter = document.getElementById('position-filter').value;
|
|
const minImpressions = parseInt(document.getElementById('impressions-filter').value) || 0;
|
|
|
|
filteredData = DATA.opportunities.filter(opp => {
|
|
// Position filter
|
|
if (positionFilter === '4-10' && (opp.position < 4 || opp.position > 10)) return false;
|
|
if (positionFilter === '11-20' && (opp.position < 11 || opp.position > 20)) return false;
|
|
|
|
// Impressions filter
|
|
if (opp.impressions < minImpressions) return false;
|
|
|
|
return true;
|
|
});
|
|
|
|
renderCards();
|
|
updateSummary();
|
|
}
|
|
|
|
// Event listeners
|
|
document.getElementById('position-filter').addEventListener('change', applyFilters);
|
|
document.getElementById('impressions-filter').addEventListener('input', applyFilters);
|
|
|
|
// Initialize
|
|
function init() {
|
|
renderCards();
|
|
updateSummary();
|
|
}
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>`,
|
|
};
|