Skip to content
20 changes: 14 additions & 6 deletions packages/code-assist/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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] -->
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this must be maps_Tools

Copy link
Author

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_Tools to preserve compatibility with the documentation system.

## 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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is search_context implemenated @caio1985 on the backend yet or still just a placeholder?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is there since v0. It is used to enrich the query to our RAG server with extra context....


<!-- [END maps_Features] -->
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

likewise this must end "maps_Tools"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Reverted to [END maps_Tools].


-----

Expand All @@ -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] -->

Expand Down Expand Up @@ -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] -->
Expand Down
55 changes: 54 additions & 1 deletion packages/code-assist/config.ts
Copy link
Member

Choose a reason for hiding this comment

The 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
Expand Up @@ -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 = [
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we'd need to keep this up to date - can we sync with list of products programmatically @mmisim ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have implemented dynamic product fetching from the /instructions endpoint in index.ts. The server now attempts to fetch and parse the product list on the first completion request, updating the cached context. It falls back to the hardcoded DEFAULT_CONTEXTS if the fetch fails.

// 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"
];
151 changes: 146 additions & 5 deletions packages/code-assist/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit brittle since it depends on the semi-unstructured XML response from the /instructions api endpoint for Code Assist middleware. What's a more robust design or any requirements for the upstream API structure or content?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, parsing the XML/Markdown structure is brittle. To mitigate this, I've implemented a fail-safe design:

  1. Hardcoded Fallback: The server initializes with a comprehensive DEFAULT_CONTEXTS list (in config.ts).
  2. Graceful Failure: If the structure changes or parsing fails, we log the error and continue using the hardcoded list. Service is never interrupted.

Ideal Upstream Change: A more robust design would be for the RAG service to return a structured JSON array (e.g., api_products) alongside the system instructions, which would eliminate the need for scraping the text.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideal Upstream Change: A more robust design would be for the RAG service to return a structured JSON array (e.g., api_products) alongside the system instructions, which would eliminate the need for scraping the text.

We can extend our /instructions endpoint to return a list of api_products but currently we're using XML as the format, we could have a ```json block inside of it with the list of products. If that helps.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a bad suggestion ☝️ @caio1985 @mmisim to consider how we always provide a consistent list of the available products via Code Assist

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. The current implementation ensures consistency via the hardcoded fallback in config.ts, while the dynamic fetch offers best-effort freshness.

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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -142,7 +214,9 @@ export const getServer = () => {
capabilities: {
tools: {},
logging: {},
resources: {}
resources: {},
prompts: {},
completions: {} // Feature: Auto-completion
},
}
);
Expand All @@ -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) {
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({
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) => {
Expand Down
16 changes: 8 additions & 8 deletions packages/code-assist/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@
"description": "Google Maps Platform Code Assist MCP (Model Context Protocol) service",
"dependencies": {
"@google-cloud/vertexai": "^1.10.0",
"@modelcontextprotocol/sdk": "^1.17.4",
"@types/node": "^22.15.18",
"axios": "1.12.0",
"@modelcontextprotocol/sdk": "^1.25.0",
"@types/node": "^22.10.0",
"axios": "^1.7.0",
"cors": "^2.8.5",
"express": "^4.19.2",
"express": "^5.2.1",
"google-auth-library": "^9.15.1",
"shx": "^0.4.0",
"typescript": "^5.8.3"
"typescript": "^5.7.0"
},
"devDependencies": {
"@types/bun": "latest",
"@types/bun": "^1.1.0",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
"bun": "1.2.18"
"@types/express": "^5.0.0",
"bun": "^1.1.0"
}
}
Loading
Loading