index.ts•16.5 kB
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
// Import knowledge bases
import { backstageOverview } from './knowledge/backstage-overview.js';
import { pluginDevelopment } from './knowledge/plugin-development.js';
import { apiReference } from './knowledge/api-reference.js';
import { communityResources } from './knowledge/community-resources.js';
import { examples } from './knowledge/examples.js';
class BackstageMCPServer {
private server: Server;
private knowledgeBase: any;
constructor() {
this.server = new Server(
{
name: 'backstage-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.knowledgeBase = {
overview: backstageOverview,
pluginDev: pluginDevelopment,
api: apiReference,
community: communityResources,
examples: examples,
};
this.setupToolHandlers();
}
private setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'get_backstage_overview',
description: 'Get comprehensive overview of Backstage framework including core features, benefits, and architecture',
inputSchema: {
type: 'object',
properties: {
section: {
type: 'string',
description: 'Specific section to retrieve (optional)',
enum: ['whatIsBackstage', 'coreFeatures', 'benefits', 'architecture']
}
}
}
},
{
name: 'get_plugin_development_guide',
description: 'Get detailed guide for developing Backstage plugins including setup, structure, and best practices',
inputSchema: {
type: 'object',
properties: {
topic: {
type: 'string',
description: 'Specific topic to retrieve (optional)',
enum: ['overview', 'gettingStarted', 'pluginStructure', 'commonPatterns', 'apis', 'testing', 'deployment', 'bestPractices']
}
}
}
},
{
name: 'get_api_reference',
description: 'Get Backstage API reference including REST endpoints, GraphQL, and client libraries',
inputSchema: {
type: 'object',
properties: {
api: {
type: 'string',
description: 'Specific API to retrieve (optional)',
enum: ['catalogApi', 'scaffolderApi', 'techDocsApi', 'authApi', 'searchApi', 'proxyApi', 'graphqlApi']
}
}
}
},
{
name: 'get_community_resources',
description: 'Get community resources, support channels, and common questions about Backstage',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Specific category to retrieve (optional)',
enum: ['officialChannels', 'communityPlugins', 'commonQuestions', 'learningResources', 'adoptionStories', 'contributing']
}
}
}
},
{
name: 'get_backstage_examples',
description: 'Get code examples and samples for common Backstage development scenarios',
inputSchema: {
type: 'object',
properties: {
type: {
type: 'string',
description: 'Type of example to retrieve (optional)',
enum: ['pluginExamples', 'catalogExamples', 'templateExamples', 'configExamples']
},
specific: {
type: 'string',
description: 'Specific example within the type (optional)'
}
}
}
},
{
name: 'search_backstage_knowledge',
description: 'Search across all Backstage knowledge for specific topics or keywords',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query for finding relevant information'
}
},
required: ['query']
}
},
{
name: 'get_plugin_scaffold_template',
description: 'Generate a plugin scaffold template with specified configuration',
inputSchema: {
type: 'object',
properties: {
pluginType: {
type: 'string',
description: 'Type of plugin to scaffold',
enum: ['frontend', 'backend', 'fullstack', 'common']
},
pluginName: {
type: 'string',
description: 'Name of the plugin'
},
features: {
type: 'array',
items: {
type: 'string',
enum: ['routing', 'api-client', 'entity-provider', 'scaffolder-action', 'search-collator']
},
description: 'Features to include in the plugin'
}
},
required: ['pluginType', 'pluginName']
}
}
] as Tool[],
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'get_backstage_overview':
return this.getBackstageOverview(args?.section as string);
case 'get_plugin_development_guide':
return this.getPluginDevelopmentGuide(args?.topic as string);
case 'get_api_reference':
return this.getApiReference(args?.api as string);
case 'get_community_resources':
return this.getCommunityResources(args?.category as string);
case 'get_backstage_examples':
return this.getBackstageExamples(args?.type as string, args?.specific as string);
case 'search_backstage_knowledge':
return this.searchBackstageKnowledge(args?.query as string);
case 'get_plugin_scaffold_template':
return this.getPluginScaffoldTemplate(args?.pluginType as string, args?.pluginName as string, args?.features as string[]);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
},
],
};
}
});
}
private getBackstageOverview(section?: string) {
const content = section ?
this.knowledgeBase.overview.content[section] :
this.knowledgeBase.overview.content;
return {
content: [
{
type: 'text',
text: JSON.stringify(content, null, 2),
},
],
};
}
private getPluginDevelopmentGuide(topic?: string) {
const content = topic ?
this.knowledgeBase.pluginDev.content[topic] :
this.knowledgeBase.pluginDev.content;
return {
content: [
{
type: 'text',
text: JSON.stringify(content, null, 2),
},
],
};
}
private getApiReference(api?: string) {
const content = api ?
this.knowledgeBase.api.content[api] :
this.knowledgeBase.api.content;
return {
content: [
{
type: 'text',
text: JSON.stringify(content, null, 2),
},
],
};
}
private getCommunityResources(category?: string) {
const content = category ?
this.knowledgeBase.community.content[category] :
this.knowledgeBase.community.content;
return {
content: [
{
type: 'text',
text: JSON.stringify(content, null, 2),
},
],
};
}
private getBackstageExamples(type?: string, specific?: string) {
let content = this.knowledgeBase.examples.content;
if (type) {
content = content[type];
if (specific && content[specific]) {
content = content[specific];
}
}
return {
content: [
{
type: 'text',
text: JSON.stringify(content, null, 2),
},
],
};
}
private searchBackstageKnowledge(query: string) {
const results: any[] = [];
const searchTerm = query.toLowerCase();
// Search through all knowledge bases
Object.entries(this.knowledgeBase).forEach(([key, knowledge]) => {
const knowledgeStr = JSON.stringify(knowledge).toLowerCase();
if (knowledgeStr.includes(searchTerm)) {
results.push({
source: key,
title: (knowledge as any).title,
relevantContent: this.extractRelevantContent(knowledge, searchTerm)
});
}
});
return {
content: [
{
type: 'text',
text: JSON.stringify({
query,
results,
totalResults: results.length
}, null, 2),
},
],
};
}
private extractRelevantContent(knowledge: any, searchTerm: string): any {
// Simple relevance extraction - in a real implementation, this could be more sophisticated
const content = JSON.stringify(knowledge.content);
const index = content.toLowerCase().indexOf(searchTerm);
if (index !== -1) {
const start = Math.max(0, index - 100);
const end = Math.min(content.length, index + 100);
return content.substring(start, end);
}
return (knowledge as any).description;
}
private getPluginScaffoldTemplate(pluginType: string, pluginName: string, features: string[] = []) {
const template = {
pluginName,
pluginType,
features,
files: this.generatePluginFiles(pluginType, pluginName, features),
commands: this.generatePluginCommands(pluginType, pluginName),
dependencies: this.generatePluginDependencies(pluginType, features)
};
return {
content: [
{
type: 'text',
text: JSON.stringify(template, null, 2),
},
],
};
}
private generatePluginFiles(pluginType: string, pluginName: string, features: string[]) {
const files: any = {};
// Base files
files[`plugins/${pluginName}/package.json`] = this.generatePackageJson(pluginName, pluginType);
files[`plugins/${pluginName}/src/index.ts`] = this.generateIndexFile(pluginType);
if (pluginType === 'frontend' || pluginType === 'fullstack') {
files[`plugins/${pluginName}/src/plugin.ts`] = this.generateFrontendPlugin(pluginName);
files[`plugins/${pluginName}/src/routes.ts`] = this.generateRoutes();
files[`plugins/${pluginName}/src/components/ExampleComponent.tsx`] = this.generateExampleComponent(pluginName);
}
if (pluginType === 'backend' || pluginType === 'fullstack') {
files[`plugins/${pluginName}-backend/src/plugin.ts`] = this.generateBackendPlugin(pluginName);
files[`plugins/${pluginName}-backend/src/router.ts`] = this.generateRouter();
}
return files;
}
private generatePackageJson(pluginName: string, pluginType: string) {
return JSON.stringify({
name: `@internal/${pluginName}${pluginType === 'backend' ? '-backend' : ''}`,
version: '0.1.0',
main: 'src/index.ts',
types: 'src/index.ts',
license: 'Apache-2.0',
dependencies: pluginType === 'frontend' ? {
'@backstage/core-components': '^0.14.0',
'@backstage/core-plugin-api': '^1.9.0',
'@backstage/theme': '^0.5.0',
'react': '^17.0.2 || ^18.0.0',
'react-router-dom': '^6.3.0'
} : {
'@backstage/backend-common': '^0.23.0',
'@backstage/backend-plugin-api': '^0.7.0',
'express': '^4.17.1',
'express-promise-router': '^4.1.0'
}
}, null, 2);
}
private generateIndexFile(pluginType: string) {
if (pluginType === 'frontend') {
return `export { ${pluginType}Plugin as default } from './plugin';`;
}
return `export * from './plugin';`;
}
private generateFrontendPlugin(pluginName: string) {
return `
import { createPlugin, createRoutableExtension } from '@backstage/core-plugin-api';
import { rootRouteRef } from './routes';
export const ${pluginName}Plugin = createPlugin({
id: '${pluginName}',
routes: {
root: rootRouteRef,
},
});
export const ${pluginName}Page = ${pluginName}Plugin.provide(
createRoutableExtension({
name: '${pluginName}Page',
component: () => import('./components/ExampleComponent').then(m => m.ExampleComponent),
mountPoint: rootRouteRef,
}),
);`;
}
private generateRoutes() {
return `import { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
id: 'root',
});`;
}
private generateExampleComponent(pluginName: string) {
return `
import React from 'react';
import { Page, Header, Content } from '@backstage/core-components';
export const ExampleComponent = () => (
<Page themeId="tool">
<Header title="${pluginName}" subtitle="Welcome to ${pluginName}!" />
<Content>
<div>Your plugin content goes here!</div>
</Content>
</Page>
);`;
}
private generateBackendPlugin(pluginName: string) {
return `
import { createBackendPlugin } from '@backstage/backend-plugin-api';
import { createRouter } from './router';
export const ${pluginName}Plugin = createBackendPlugin({
pluginId: '${pluginName}',
register(env) {
env.registerInit({
deps: {
httpRouter: coreServices.httpRouter,
logger: coreServices.logger,
},
async init({ httpRouter, logger }) {
httpRouter.use(await createRouter({ logger }));
},
});
},
});`;
}
private generateRouter() {
return `
import { Router } from 'express';
import { Logger } from 'winston';
export interface RouterOptions {
logger: Logger;
}
export async function createRouter(options: RouterOptions): Promise<Router> {
const { logger } = options;
const router = Router();
router.get('/health', (_, response) => {
logger.info('PONG!');
response.json({ status: 'ok' });
});
return router;
}`;
}
private generatePluginCommands(pluginType: string, pluginName: string) {
return [
`yarn create @backstage/plugin --${pluginType} ${pluginName}`,
`cd plugins/${pluginName}`,
'yarn install',
'yarn build',
'yarn test'
];
}
private generatePluginDependencies(pluginType: string, features: string[]) {
const baseDeps = pluginType === 'frontend' ? [
'@backstage/core-components',
'@backstage/core-plugin-api',
'@backstage/theme'
] : [
'@backstage/backend-common',
'@backstage/backend-plugin-api'
];
const featureDeps: { [key: string]: string[] } = {
'api-client': ['@backstage/catalog-client'],
'entity-provider': ['@backstage/plugin-catalog-backend'],
'scaffolder-action': ['@backstage/plugin-scaffolder-backend'],
'search-collator': ['@backstage/plugin-search-backend-node']
};
const additionalDeps = features.flatMap(feature => featureDeps[feature] || []);
return [...baseDeps, ...additionalDeps];
}
async start() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Backstage MCP server running on stdio');
// Keep process alive - MCP servers should run indefinitely
process.on('SIGINT', () => {
console.error('Shutting down Backstage MCP server...');
process.exit(0);
});
process.on('SIGTERM', () => {
console.error('Shutting down Backstage MCP server...');
process.exit(0);
});
}
}
const server = new BackstageMCPServer();
server.start().catch(console.error);