vaadin-docs-mcp-server
Version:
MCP server for Vaadin documentation with document-based search and full document retrieval
420 lines (419 loc) • 18.5 kB
JavaScript
/**
* Vaadin Documentation MCP Server
*
* This server provides access to Vaadin documentation through the Model Context Protocol.
* It allows IDE assistants and developers to search for relevant documentation and navigate
* hierarchical relationships between documents using parent-child links.
* Uses stdio transport for local connections and defers queries to the REST server.
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js';
import { config } from './config.js';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
/**
* Vaadin Documentation MCP Server
*/
class VaadinDocsServer {
server;
constructor() {
// Initialize MCP server
this.server = new Server({
name: config.server.name,
version: config.server.version
}, {
capabilities: {
tools: {}
}
});
// Set up tool handlers
this.setupToolHandlers();
// Error handling
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
/**
* Set up tool handlers
*/
setupToolHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'get_vaadin_primer',
description: '🚨 IMPORTANT: Always use this tool FIRST before working with Vaadin. Returns a comprehensive primer document with current (2024+) information about modern Vaadin development. This addresses common AI misconceptions about Vaadin and provides up-to-date information about Flow vs Hilla, project structure, components, and best practices. Essential reading to avoid outdated assumptions.',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false
}
},
{
name: 'search_vaadin_docs',
description: 'Search Vaadin documentation for relevant information about Vaadin development, components, and best practices. ⚠️ IMPORTANT: Use get_vaadin_primer FIRST to understand modern Vaadin before searching. This tool returns search results that include file_path information for complete document retrieval. When using this tool, try to deduce the correct framework from context: use "flow" for Java-based views, "hilla" for React-based views, or empty string for both frameworks. Use get_full_document with the file_path from results when you need complete context.',
inputSchema: {
type: 'object',
properties: {
question: {
type: 'string',
description: 'The search query or question about Vaadin. Will be used to query a vector database with hybrid search (semantic + keyword).'
},
max_results: {
type: 'number',
description: 'Maximum number of results to return (default: 5)',
minimum: 1,
maximum: 20
},
max_tokens: {
type: 'number',
description: 'Maximum number of tokens to return (default: 1500)',
minimum: 100,
maximum: 5000
},
framework: {
type: 'string',
description: 'The Vaadin framework to focus on: "flow" for Java-based views, "hilla" for React-based views, or empty string for both. If not specified, the agent should try to deduce the correct framework from context or asking the user for clarification.',
enum: ['flow', 'hilla', '']
}
},
required: ['question']
}
},
{
name: 'get_full_document',
description: 'Retrieves complete documentation pages for one or more file paths. Use this when you need full context beyond what search results provide. ⚠️ IMPORTANT: Use get_vaadin_primer FIRST to understand modern Vaadin fundamentals. After finding relevant chunks via search_vaadin_docs, use this to get complete context, examples, and cross-references. The response includes the complete markdown content with full context. Supports fetching multiple files at once to reduce roundtrips.',
inputSchema: {
type: 'object',
properties: {
file_path: {
type: 'string',
description: 'A single file path from search results (e.g., "building-apps/forms-data/add-form/fields-and-binding/hilla.md"). Use this for fetching a single document.'
},
file_paths: {
type: 'array',
items: {
type: 'string'
},
description: 'An array of file paths from search results. Use this for fetching multiple documents at once to reduce roundtrips.'
}
},
anyOf: [
{ required: ['file_path'] },
{ required: ['file_paths'] }
]
}
},
{
name: 'get_vaadin_version',
description: 'Returns the latest stable version of Vaadin Core as a simple JSON object. This is useful when setting up new projects, checking for updates, or when helping with dependency management. Returns: {version, released}.',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false
}
}
]
}));
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case 'get_vaadin_primer':
return this.handleGetVaadinPrimerTool();
case 'search_vaadin_docs':
return this.handleSearchTool(request.params.arguments);
case 'get_full_document':
return this.handleGetFullDocumentTool(request.params.arguments);
case 'get_vaadin_version':
return this.handleGetVaadinVersionTool();
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
}
});
}
/**
* Handle get_vaadin_primer tool
*/
async handleGetVaadinPrimerTool() {
try {
// Get the path to the primer document (ES module compatible)
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const primerPath = path.join(__dirname, 'vaadin-primer.md');
// Read the primer document
const primerContent = fs.readFileSync(primerPath, 'utf-8');
return {
content: [
{
type: 'text',
text: primerContent
}
]
};
}
catch (error) {
console.error('Error reading Vaadin primer:', error);
return {
content: [
{
type: 'text',
text: `Error reading Vaadin primer: ${error instanceof Error ? error.message : 'Unknown error'}`
}
],
isError: true
};
}
}
/**
* Handle search_vaadin_docs tool
*/
async handleSearchTool(args) {
// Validate arguments
if (!args.question || typeof args.question !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid question parameter');
}
try {
// Forward request to REST server
const response = await fetch(`${config.restServer.url}/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
question: args.question, // Use 'question' for the enhanced API
max_results: args.max_results,
max_tokens: args.max_tokens,
framework: args.framework || ''
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error ${response.status}`);
}
const data = await response.json();
// Format results with hierarchical information
const formattedResults = this.formatSearchResults(data.results);
return {
content: [
{
type: 'text',
text: formattedResults
}
]
};
}
catch (error) {
console.error('Error searching documentation:', error);
return {
content: [
{
type: 'text',
text: `Error searching Vaadin documentation: ${error instanceof Error ? error.message : 'Unknown error'}`
}
],
isError: true
};
}
}
/**
* Handle get_full_document tool
*/
async handleGetFullDocumentTool(args) {
// Validate arguments
if (!args.file_path && !args.file_paths) {
throw new McpError(ErrorCode.InvalidParams, 'Missing file_path or file_paths parameter');
}
// Validate file_path if provided
if (args.file_path && typeof args.file_path !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'file_path must be a string');
}
// Validate file_paths if provided
if (args.file_paths && (!Array.isArray(args.file_paths) || args.file_paths.some((path) => typeof path !== 'string'))) {
throw new McpError(ErrorCode.InvalidParams, 'file_paths must be an array of strings');
}
// Determine file paths to fetch
const filePaths = args.file_paths || [args.file_path];
try {
// Fetch all documents in parallel
const fetchPromises = filePaths.map(async (filePath) => {
const response = await fetch(`${config.restServer.url}/document/${encodeURIComponent(filePath)}`);
if (!response.ok) {
if (response.status === 404) {
return {
error: `Document with file path "${filePath}" not found`,
filePath
};
}
const errorData = await response.json();
return {
error: errorData.error || `HTTP error ${response.status} for ${filePath}`,
filePath
};
}
const document = await response.json();
return {
document,
filePath
};
});
const results = await Promise.all(fetchPromises);
// Format the results
const formattedResults = this.formatFullDocuments(results);
return {
content: [
{
type: 'text',
text: formattedResults
}
]
};
}
catch (error) {
console.error('Error fetching full documents:', error);
return {
content: [
{
type: 'text',
text: `Error fetching full documents: ${error instanceof Error ? error.message : 'Unknown error'}`
}
],
isError: true
};
}
}
/**
* Handle get_vaadin_version tool
*/
async handleGetVaadinVersionTool() {
try {
// Forward request to REST server
const response = await fetch(`${config.restServer.url}/vaadin-version`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error ${response.status}`);
}
const data = await response.json();
// Return simple JSON structure with only version and release timestamp
const versionInfo = {
version: data.version,
released: data.released
};
return {
content: [
{
type: 'text',
text: JSON.stringify(versionInfo, null, 2)
}
]
};
}
catch (error) {
console.error('Error fetching Vaadin version:', error);
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: `Failed to fetch Vaadin version: ${error instanceof Error ? error.message : 'Unknown error'}`
}, null, 2)
}
],
isError: true
};
}
}
/**
* Format search results for display with document information
* @param results - Search results from the enhanced API
* @returns Formatted results as a string
*/
formatSearchResults(results) {
if (results.length === 0) {
return 'No relevant documentation found.';
}
let output = `Found ${results.length} relevant documentation sections:\n\n`;
results.forEach((result, index) => {
output += `## ${index + 1}. ${result.metadata?.title || 'Untitled'}\n`;
// Format metadata as markdown front matter
output += `----\n`;
output += `Source: ${result.source_url}\n`;
output += `Framework: ${result.framework}\n`;
output += `Chunk ID: ${result.chunk_id}\n`;
if (result.file_path) {
output += `Document Path: ${result.file_path} (use get_full_document to get complete context)\n`;
}
output += `Relevance Score: ${result.relevance_score.toFixed(3)}\n`;
output += `----\n\n`;
output += `${result.content}\n\n`;
if (index < results.length - 1) {
output += '================\n\n';
}
});
return output;
}
/**
* Format a full document for display
* @param document - Complete document data from the API
* @returns Formatted document as a string
*/
formatFullDocument(document) {
let output = `# ${document.metadata?.title || 'Documentation'}\n\n`;
output += `----\n`;
output += `File Path: ${document.file_path}\n`;
output += `Framework: ${document.metadata?.framework || 'unknown'}\n`;
output += `Source URL: ${document.metadata?.source_url || 'N/A'}\n`;
output += `----\n\n`;
output += `## Complete Documentation\n\n${document.content}\n`;
return output;
}
/**
* Format multiple full documents for display
* @param results - Array of result objects that may contain document or error
* @returns Formatted documents as a string
*/
formatFullDocuments(results) {
if (results.length === 0) {
return 'No documents found.';
}
let output = `Found ${results.length} document${results.length > 1 ? 's' : ''}:\n\n`;
results.forEach((result, index) => {
if (result.error) {
output += `## ${index + 1}. Error fetching document\n`;
output += `----\n`;
output += `File Path: ${result.filePath}\n`;
output += `Error: ${result.error}\n`;
output += `----\n\n`;
}
else {
output += `## ${index + 1}. ${result.document.metadata?.title || 'Untitled'}\n`;
output += `----\n`;
output += `File Path: ${result.filePath}\n`;
output += `Framework: ${result.document.metadata?.framework || 'unknown'}\n`;
output += `Source URL: ${result.document.metadata?.source_url || 'N/A'}\n`;
output += `----\n\n`;
output += `### Complete Documentation\n\n${result.document.content}\n\n`;
}
if (index < results.length - 1) {
output += '================\n\n';
}
});
return output;
}
/**
* Run the server with stdio transport
*/
async run() {
// Create a new stdio transport
const transport = new StdioServerTransport();
// Connect the server to the transport
await this.server.connect(transport);
}
}
// Create and run the server
const server = new VaadinDocsServer();
server.run().catch(console.error);