( server: McpServer, context: McpAuthContext, )
| 72 | } |
| 73 | |
| 74 | export function registerGscQueryTools( |
| 75 | server: McpServer, |
| 76 | context: McpAuthContext, |
| 77 | ) { |
| 78 | server.tool( |
| 79 | 'gsc_get_top_queries', |
| 80 | 'Get the top search queries driving traffic from Google Search, ranked by clicks. Includes impressions, CTR, and average position for each query.', |
| 81 | { |
| 82 | projectId: projectIdSchema(context), |
| 83 | ...zDateRange, |
| 84 | limit: z |
| 85 | .number() |
| 86 | .min(1) |
| 87 | .max(1000) |
| 88 | .default(100) |
| 89 | .optional() |
| 90 | .describe('Maximum number of queries to return (1-1000, default 100)'), |
| 91 | }, |
| 92 | async ({ projectId: inputProjectId, startDate: sd, endDate: ed, limit }) => |
| 93 | withErrorHandling(async () => { |
| 94 | const projectId = await resolveProjectId(context, inputProjectId); |
| 95 | const { startDate, endDate } = resolveDateRange(sd, ed); |
| 96 | return getGscQueries(projectId, startDate, endDate, limit ?? 100); |
| 97 | }), |
| 98 | ); |
| 99 | |
| 100 | server.tool( |
| 101 | 'gsc_get_query_opportunities', |
| 102 | 'Identify low-hanging-fruit SEO opportunities: queries ranking on positions 4-20 with meaningful search volume where small improvements could yield significant traffic gains. Ranked by opportunity score.', |
| 103 | { |
| 104 | projectId: projectIdSchema(context), |
| 105 | ...zDateRange, |
| 106 | minImpressions: z |
| 107 | .number() |
| 108 | .min(1) |
| 109 | .default(50) |
| 110 | .optional() |
| 111 | .describe( |
| 112 | 'Minimum impression threshold to filter out low-volume queries (default: 50)', |
| 113 | ), |
| 114 | }, |
| 115 | async ({ projectId: inputProjectId, startDate: sd, endDate: ed, minImpressions }) => |
| 116 | withErrorHandling(async () => { |
| 117 | const projectId = await resolveProjectId(context, inputProjectId); |
| 118 | const { startDate, endDate } = resolveDateRange(sd, ed); |
| 119 | const queries = await getGscQueries(projectId, startDate, endDate, 5000); |
| 120 | const filtered = queries.filter( |
| 121 | (q) => q.impressions >= (minImpressions ?? 50), |
| 122 | ); |
| 123 | const opportunities = computeOpportunities(filtered); |
| 124 | return { |
| 125 | opportunities, |
| 126 | total_analyzed: filtered.length, |
| 127 | min_impressions: minImpressions ?? 50, |
| 128 | }; |
| 129 | }), |
| 130 | ); |
| 131 |
no test coverage detected