-
Notifications
You must be signed in to change notification settings - Fork 23
Upgrade dependencies for code-assist MCP server #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
2f05465
718480f
a563c28
238283e
4e22fae
35c30d0
50ef9a4
5a73c06
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,14 +48,22 @@ Below is an example MCP Client response to a user's question with Code Assist MC | |
|
|
||
| ----- | ||
|
|
||
| <!-- [START maps_Tools] --> | ||
| ## Tools Provided | ||
| <!-- [START maps_Features] --> | ||
| ## MCP Features Provided | ||
|
|
||
| The MCP server exposes the following tools for AI clients: | ||
| The MCP server exposes the following capabilities for AI clients: | ||
|
|
||
| ### Tools | ||
| 1. **`retrieve-instructions`**: A helper tool used by the client to get crucial system instructions on how to best reason about user intent and formulate effective calls to the `retrieve-google-maps-platform-docs` tool. | ||
| 2. **`retrieve-google-maps-platform-docs`**: The primary tool. It takes a natural language query and submits it to a hosted Retrieval Augmented Generation (RAG) engine. The RAG engine searches fresh versions of official Google Maps Platform documentation, tutorials, and code samples, returning relevant context to the AI to generate an accurate response. | ||
| <!-- [END maps_Tools] --> | ||
|
|
||
| ### Prompts | ||
| 1. **`code-assist`**: A prompt template that pre-configures the AI assistant with expert instructions and best practices for Google Maps Platform development. It accepts an optional `task` argument. | ||
|
|
||
| ### Completion | ||
| - The server provides auto-completion for the `retrieve-google-maps-platform-docs` tool arguments (specifically `search_context`), helping users discover valid Google Maps Platform products and features. | ||
|
||
|
|
||
| <!-- [END maps_Features] --> | ||
|
||
|
|
||
| ----- | ||
|
|
||
|
|
@@ -66,7 +74,7 @@ The MCP server exposes the following tools for AI clients: | |
| This server supports two standard MCP communication protocols: | ||
|
|
||
| * **`stdio`**: This is the default transport used when a client invokes the server via a `command`. It communicates over the standard input/output streams, making it ideal for local command-line execution. | ||
| * **`Streamable HTTP`**: The server exposes a `/mcp` endpoint that accepts POST requests. This is used by clients that connect via a `url` and is the standard for remote server connections. Our implementation supports streaming for real-time, interactive responses. | ||
| * **`Streamable HTTP`**: The server exposes a `/mcp` endpoint that accepts POST requests and SSE connections. This is used by clients that connect via a `url` and is the standard for remote server connections. Our implementation supports streaming for real-time, interactive responses. | ||
|
|
||
| <!-- [END maps_Transports] --> | ||
|
|
||
|
|
@@ -383,7 +391,7 @@ curl -X POST http://localhost:3215/mcp \ | |
| The server will respond with an SSE event containing its capabilities. | ||
| ``` | ||
| event: message | ||
| data: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{},"logging":{},"resources":{}},"serverInfo":{"name":"code-assist-mcp","version":"0.1.3"}}} | ||
| data: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"tools":{},"logging":{},"resources":{},"prompts":{},"completions":{}},"serverInfo":{"name":"code-assist-mcp","version":"0.1.7"}}} | ||
| ``` | ||
|
|
||
| <!-- [END maps_StreamableHTTP_Guide] --> | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DEFAULT_CONTEXTS are the search_contexts related to the user query. If you add all GMP products it will result into undesired results as the RAG will bring documentation from products unrelated to the user query. See usage of mergedContexts and contexts on index.ts. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,4 +18,57 @@ export const ragEndpoint = "https://rag-230009110455.us-central1.run.app" | |
|
|
||
| export const SOURCE = process.env.SOURCE || 'github'; | ||
|
|
||
| export const DEFAULT_CONTEXTS = ["Google Maps Platform"]; | ||
| export const DEFAULT_CONTEXTS = [ | ||
|
||
| // General | ||
| "Google Maps Platform", | ||
|
|
||
| // Maps | ||
| "Maps JavaScript API", | ||
| "Maps SDK for Android", | ||
| "Maps SDK for iOS", | ||
| "Google Maps for Flutter", | ||
| "Maps Embed API", | ||
| "Maps Static API", | ||
| "Street View Static API", | ||
| "Maps URLs", | ||
| "Elevation API", | ||
| "Map Tiles API", | ||
| "Maps Datasets API", | ||
| "Web Components", | ||
| "3D Maps", | ||
| "Aerial View API", | ||
|
|
||
| // Routes | ||
| "Routes API", | ||
| "Directions API", | ||
| "Distance Matrix API", | ||
| "Navigation SDK for Android", | ||
| "Navigation SDK for iOS", | ||
| "Navigation for Flutter", | ||
| "Navigation for React Native", | ||
| "Roads API", | ||
| "Route Optimization API", | ||
|
|
||
| // Places | ||
| "Places UI Kit", | ||
| "Places API (New)", | ||
| "Places API (Legacy)", | ||
| "Places SDK for Android", | ||
| "Places SDK for iOS", | ||
| "Places Library", | ||
| "Geocoding API", | ||
| "Geolocation API", | ||
| "Address Validation API", | ||
| "Time Zone API", | ||
|
|
||
| // Environment | ||
| "Air Quality API", | ||
| "Pollen API", | ||
| "Solar API", | ||
| "Weather API", | ||
|
|
||
| // Analytics | ||
| "Imagery Insights", | ||
| "Places Insights", | ||
| "Road Management Insights" | ||
| ]; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,10 +21,70 @@ import { randomUUID } from "node:crypto"; | |
| import { Server } from '@modelcontextprotocol/sdk/server/index.js'; | ||
| import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; | ||
| import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; | ||
| import { Tool, CallToolRequest, CallToolRequestSchema, ListToolsRequestSchema, Resource, ListResourcesRequestSchema, ReadResourceRequest, ReadResourceRequestSchema, isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; | ||
| import { | ||
| Tool, | ||
| CallToolRequest, | ||
| CallToolRequestSchema, | ||
| ListToolsRequestSchema, | ||
| Resource, | ||
| ListResourcesRequestSchema, | ||
| ReadResourceRequest, | ||
| ReadResourceRequestSchema, | ||
| isInitializeRequest, | ||
| ListPromptsRequestSchema, | ||
| GetPromptRequestSchema, | ||
| GetPromptRequest, | ||
| CompleteRequestSchema, | ||
| CompleteRequest, | ||
| Prompt | ||
| } from '@modelcontextprotocol/sdk/types.js'; | ||
| import { ragEndpoint, DEFAULT_CONTEXTS, SOURCE } from './config.js'; | ||
| import axios from 'axios'; | ||
|
|
||
| // Cache for products list | ||
|
||
| let cachedProducts: string[] = [...DEFAULT_CONTEXTS]; | ||
| let productsFetched = false; | ||
|
|
||
| // Function to fetch and parse products from instructions | ||
| async function fetchProductsFromInstructions() { | ||
| if (productsFetched) return; | ||
|
|
||
| try { | ||
| const response = await axios.get(ragEndpoint.concat("/instructions"), { | ||
| params: { source: SOURCE } | ||
| }); | ||
|
|
||
| const systemInstructions = response.data?.systemInstructions; | ||
| if (!systemInstructions) return; | ||
|
|
||
| // Extract content between <product_overview> tags | ||
| const match = systemInstructions.match(/<product_overview>([\s\S]*?)<\/product_overview>/); | ||
| if (!match) return; | ||
|
|
||
| const content = match[1]; | ||
| // Regex to find product names in format: * **Product Name** | ||
| const productRegex = /\*\s+\*\*([^*]+)\*\*/g; | ||
| const newProducts: string[] = []; | ||
|
|
||
| let productMatch; | ||
| while ((productMatch = productRegex.exec(content)) !== null) { | ||
| if (productMatch[1]) { | ||
| newProducts.push(productMatch[1].trim()); | ||
| } | ||
| } | ||
|
|
||
| if (newProducts.length > 0) { | ||
| // Merge with default contexts, removing duplicates | ||
| const uniqueProducts = new Set([...cachedProducts, ...newProducts]); | ||
| cachedProducts = Array.from(uniqueProducts); | ||
| productsFetched = true; | ||
| console.log(`Fetched ${newProducts.length} products from instructions.`); | ||
| } | ||
| } catch (error) { | ||
| console.error("Failed to fetch products from instructions:", error); | ||
| } | ||
| } | ||
|
|
||
| // MCP Streamable HTTP compliance: Accept header validation | ||
| function validateAcceptHeader(req: Request): boolean { | ||
| const acceptHeader = req.headers.accept; | ||
|
|
@@ -95,6 +155,18 @@ const instructionsResource: Resource = { | |
| description: 'Contains critical system instructions and context for Google Maps Platform (APIs for maps, routes, and places), Location Analytics, Google Earth, and Google Earth Engine. You MUST load this resource or call the `retrieve-instructions` tool before using any other tool, especially `retrieve-google-maps-platform-docs`, to understand how to handle location-based use cases.' | ||
| }; | ||
|
|
||
| const CodeAssistPrompt: Prompt = { | ||
| name: "code-assist", | ||
| description: "Sets up the context for Google Maps Platform coding assistance, including system instructions and best practices.", | ||
| arguments: [ | ||
| { | ||
| name: "task", | ||
| description: "The specific task or question the user needs help with.", | ||
| required: false | ||
| } | ||
| ] | ||
| }; | ||
|
|
||
| let usageInstructions: any = null; | ||
|
|
||
| // Session management for StreamableHTTP transport | ||
|
|
@@ -142,7 +214,9 @@ export const getServer = () => { | |
| capabilities: { | ||
| tools: {}, | ||
| logging: {}, | ||
| resources: {} | ||
| resources: {}, | ||
| prompts: {}, | ||
| completions: {} // Feature: Auto-completion | ||
| }, | ||
| } | ||
| ); | ||
|
|
@@ -159,9 +233,74 @@ export const getServer = () => { | |
| server.setRequestHandler(ReadResourceRequestSchema, (request) => handleReadResource(request, server)); | ||
| server.setRequestHandler(CallToolRequestSchema, (request) => handleCallTool(request, server)); | ||
|
|
||
| server.setRequestHandler(ListPromptsRequestSchema, async () => ({ | ||
| prompts: [CodeAssistPrompt] | ||
| })); | ||
|
|
||
| server.setRequestHandler(GetPromptRequestSchema, (request) => handleGetPrompt(request, server)); | ||
|
|
||
| server.setRequestHandler(CompleteRequestSchema, (request) => handleCompletion(request, server)); | ||
|
|
||
| return server; | ||
| }; | ||
|
|
||
| export async function handleGetPrompt(request: GetPromptRequest, server: Server) { | ||
ryanbaumann marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (request.params.name === "code-assist") { | ||
| const instructions = await getUsageInstructions(server); | ||
| if (!instructions) { | ||
| throw new Error("Could not retrieve instructions for prompt"); | ||
| } | ||
|
|
||
| const task = request.params.arguments?.task; | ||
| const promptText = `Please act as a Google Maps Platform expert using the following instructions:\n\n${instructions.join('\n\n')}${task ? `\n\nTask: ${task}` : ''}`; | ||
|
|
||
| return { | ||
| messages: [ | ||
| { | ||
| role: "user", | ||
| content: { | ||
| type: "text", | ||
| text: promptText | ||
| } | ||
| } | ||
| ] | ||
| }; | ||
| } | ||
| throw new Error(`Prompt not found: ${request.params.name}`); | ||
| } | ||
|
|
||
| export async function handleCompletion(request: CompleteRequest, server: Server) { | ||
| if (request.params.ref.type === "ref/tool" && | ||
| request.params.ref.name === "retrieve-google-maps-platform-docs" && | ||
| request.params.argument.name === "search_context") { | ||
|
|
||
| // Try to refresh products if not yet fetched (background) | ||
| if (!productsFetched) { | ||
| fetchProductsFromInstructions().catch(e => console.error(e)); | ||
| } | ||
|
|
||
| const currentInput = request.params.argument.value.toLowerCase(); | ||
| // Filter cachedProducts based on input | ||
| const matches = cachedProducts.filter(ctx => ctx.toLowerCase().includes(currentInput)); | ||
|
|
||
| return { | ||
| completion: { | ||
| values: matches.slice(0, 10), // Limit to top 10 matches | ||
| total: matches.length, | ||
| hasMore: matches.length > 10 | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| completion: { | ||
| values: [], | ||
| total: 0, | ||
| hasMore: false | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| export async function handleReadResource(request: ReadResourceRequest, server: Server) { | ||
| if (request.params.uri === instructionsResource.uri) { | ||
| server.sendLoggingMessage({ | ||
|
|
@@ -213,8 +352,9 @@ export async function handleCallTool(request: CallToolRequest, server: Server) { | |
| let prompt: string = request.params.arguments?.prompt as string; | ||
| let searchContext: string[] = request.params.arguments?.search_context as string[]; | ||
|
|
||
| // Merge searchContext with DEFAULT_CONTEXTS and remove duplicates | ||
| const mergedContexts = new Set([...DEFAULT_CONTEXTS, ...(searchContext || [])]); | ||
| // Merge searchContext with cachedProducts and remove duplicates. | ||
| // Note: We use the cached product list here as the base context, not just the static DEFAULT_CONTEXTS. | ||
| const mergedContexts = new Set([...cachedProducts, ...(searchContext || [])]); | ||
| const contexts = Array.from(mergedContexts); | ||
|
|
||
| // Log user request for debugging purposes | ||
|
|
@@ -328,7 +468,8 @@ async function runServer() { | |
|
|
||
| if (sessionId && transports.has(sessionId)) { | ||
| transport = transports.get(sessionId)!; | ||
| } else if (!sessionId && isInitializeRequest(req.body)) { | ||
| } else if (!sessionId && (req.method === 'GET' || isInitializeRequest(req.body))) { | ||
| // Create a new session for GET (SSE connection) or POST (initialize request) | ||
| transport = new StreamableHTTPServerTransport({ | ||
| sessionIdGenerator: () => randomUUID(), | ||
| onsessioninitialized: (newSessionId) => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this must be
maps_ToolsThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. I reverted the marker name to
maps_Toolsto preserve compatibility with the documentation system.