server.ts•11.5 kB
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
type Tool,
} from '@modelcontextprotocol/sdk/types.js';
import { NewRelicClient } from './client/newrelic-client';
import { AlertTool } from './tools/alert';
import { ApmTool } from './tools/apm';
import { EntityTool } from './tools/entity';
import { NerdGraphTool } from './tools/nerdgraph';
import { NrqlTool } from './tools/nrql';
import { RestApmTool } from './tools/rest/apm';
import { RestDeploymentsTool } from './tools/rest/deployments';
import { RestMetricsTool } from './tools/rest/metrics';
import { SyntheticsTool } from './tools/synthetics';
export class NewRelicMCPServer {
private server: Server;
private client: NewRelicClient;
private tools: Map<string, Tool>;
private defaultAccountId?: string;
constructor(client?: NewRelicClient) {
this.defaultAccountId = process.env.NEW_RELIC_ACCOUNT_ID;
if (client) {
this.client = client;
} else {
// Best practice for Smithery tool discovery: do not force auth at startup
// Allow listing tools without credentials; validate when tools are invoked
const apiKey = process.env.NEW_RELIC_API_KEY || '';
this.client = new NewRelicClient(apiKey, this.defaultAccountId);
}
this.server = new Server(
{
name: 'newrelic-mcp',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.tools = new Map();
this.registerTools();
this.setupHandlers();
}
private registerTools(): void {
const nrqlTool = new NrqlTool(this.client);
const apmTool = new ApmTool(this.client);
const entityTool = new EntityTool(this.client);
const alertTool = new AlertTool(this.client);
const syntheticsTool = new SyntheticsTool(this.client);
const nerdGraphTool = new NerdGraphTool(this.client);
const restDeployments = new RestDeploymentsTool();
const restApm = new RestApmTool();
const restMetrics = new RestMetricsTool();
// Register all tools
const tools = [
nrqlTool.getToolDefinition(),
apmTool.getListApplicationsTool(),
entityTool.getSearchTool(),
entityTool.getDetailsTool(),
alertTool.getPoliciesTool(),
alertTool.getIncidentsTool(),
alertTool.getAcknowledgeTool(),
syntheticsTool.getListMonitorsTool(),
syntheticsTool.getCreateMonitorTool(),
nerdGraphTool.getQueryTool(),
// REST v2 tools
restDeployments.getCreateTool(),
restDeployments.getListTool(),
restDeployments.getDeleteTool(),
restApm.getListApplicationsTool(),
restMetrics.getListMetricNamesTool(),
restMetrics.getMetricDataTool(),
restMetrics.getListApplicationHostsTool(),
{
name: 'get_account_details',
description: 'Get New Relic account details',
inputSchema: {
type: 'object' as const,
properties: {
target_account_id: {
type: 'string' as const,
description: 'Optional account ID to get details for',
},
},
},
},
];
tools.forEach((tool) => {
this.tools.set(tool.name, tool);
});
}
private setupHandlers(): void {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: Array.from(this.tools.values()),
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const tool = this.tools.get(request.params.name);
if (!tool) {
throw new McpError(ErrorCode.MethodNotFound, `Tool ${request.params.name} not found`);
}
try {
const result = await this.executeTool(request.params.name, request.params.arguments || {});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : 'Tool execution failed';
throw new McpError(ErrorCode.InternalError, message);
}
});
}
async start(): Promise<void> {
// Only validate if credentials were provided; otherwise allow startup for tool discovery
if (process.env.NEW_RELIC_API_KEY) {
const isValid = await this.client.validateCredentials();
if (!isValid) {
throw new Error('Invalid New Relic API credentials');
}
}
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('New Relic MCP Server started');
}
async executeTool(
name: string,
args: { target_account_id?: string; account_id?: string; [key: string]: unknown }
): Promise<unknown> {
const accountId: string | undefined =
args.target_account_id || args.account_id || this.defaultAccountId;
if (!accountId && this.requiresAccountId(name)) {
throw new Error('Account ID must be provided');
}
switch (name) {
case 'run_nrql_query':
return await new NrqlTool(this.client).execute({
...args,
target_account_id: accountId,
});
case 'list_apm_applications':
return await new ApmTool(this.client).execute({
...args,
target_account_id: accountId,
});
case 'create_deployment':
return await new RestDeploymentsTool().create(
args as Parameters<RestDeploymentsTool['create']>[0]
);
case 'list_deployments_rest':
return await new RestDeploymentsTool().list(
args as Parameters<RestDeploymentsTool['list']>[0]
);
case 'delete_deployment':
return await new RestDeploymentsTool().delete(
args as Parameters<RestDeploymentsTool['delete']>[0]
);
case 'list_apm_applications_rest':
return await new RestApmTool().listApplications(
args as Parameters<RestApmTool['listApplications']>[0]
);
case 'list_metric_names_for_host':
return await new RestMetricsTool().listMetricNames(
args as Parameters<RestMetricsTool['listMetricNames']>[0]
);
case 'get_metric_data_for_host':
return await new RestMetricsTool().getMetricData(
args as Parameters<RestMetricsTool['getMetricData']>[0]
);
case 'list_application_hosts':
return await new RestMetricsTool().listApplicationHosts(
args as Parameters<RestMetricsTool['listApplicationHosts']>[0]
);
case 'get_account_details':
return await this.client.getAccountDetails(accountId);
case 'list_alert_policies':
return await new AlertTool(this.client).listAlertPolicies({
...args,
target_account_id: accountId,
});
case 'list_open_incidents':
return await new AlertTool(this.client).listOpenIncidents({
...args,
target_account_id: accountId,
});
case 'acknowledge_incident': {
const { incident_id, comment } = args as Record<string, unknown>;
if (typeof incident_id !== 'string' || incident_id.trim() === '') {
throw new Error('acknowledge_incident: "incident_id" (non-empty string) is required');
}
if (comment !== undefined && typeof comment !== 'string') {
throw new Error('acknowledge_incident: "comment" must be a string when provided');
}
return await new AlertTool(this.client).acknowledgeIncident({
incident_id,
comment: comment as string | undefined,
});
}
case 'search_entities': {
const { query, entity_types } = args as Record<string, unknown>;
if (typeof query !== 'string' || query.trim() === '') {
throw new Error('search_entities: "query" (non-empty string) is required');
}
let types: string[] | undefined;
if (entity_types !== undefined) {
if (!Array.isArray(entity_types)) {
throw new Error('search_entities: "entity_types" must be an array of strings');
}
types = (entity_types as unknown[]).filter((t): t is string => typeof t === 'string');
}
return await new EntityTool(this.client).searchEntities({
query,
entity_types: types,
target_account_id: accountId,
});
}
case 'get_entity_details': {
const { entity_guid } = args as Record<string, unknown>;
if (typeof entity_guid !== 'string' || entity_guid.trim() === '') {
throw new Error('get_entity_details: "entity_guid" (non-empty string) is required');
}
return await new EntityTool(this.client).getEntityDetails({ entity_guid });
}
case 'list_synthetics_monitors':
return await new SyntheticsTool(this.client).listSyntheticsMonitors({
...args,
target_account_id: accountId,
});
case 'create_browser_monitor': {
const { name, url, frequency, locations } = args as Record<string, unknown>;
if (typeof name !== 'string' || name.trim() === '') {
throw new Error('create_browser_monitor: "name" (non-empty string) is required');
}
if (typeof url !== 'string' || url.trim() === '') {
throw new Error('create_browser_monitor: "url" (non-empty string) is required');
}
if (typeof frequency !== 'number' || !Number.isFinite(frequency) || frequency <= 0) {
throw new Error('create_browser_monitor: "frequency" (positive number) is required');
}
if (
!Array.isArray(locations) ||
(locations as unknown[]).some((l) => typeof l !== 'string')
) {
throw new Error('create_browser_monitor: "locations" must be an array of strings');
}
return await new SyntheticsTool(this.client).createBrowserMonitor({
name,
url,
frequency,
locations: locations as string[],
target_account_id: accountId,
});
}
case 'run_nerdgraph_query':
return await new NerdGraphTool(this.client).execute(args);
default: {
const tool = this.tools.get(name);
if (!tool) {
throw new Error(`Tool ${name} not found`);
}
throw new Error(`Tool handler for ${name} not implemented`);
}
}
}
private requiresAccountId(toolName: string): boolean {
const accountRequiredTools = [
'run_nrql_query',
'list_apm_applications',
'search_entities',
'get_account_details',
'list_alert_policies',
'list_open_incidents',
'list_synthetics_monitors',
'create_browser_monitor',
];
return accountRequiredTools.includes(toolName);
}
getMetadata() {
return {
name: 'newrelic-mcp',
version: '1.0.0',
description: 'MCP server for New Relic observability platform integration',
};
}
getRegisteredTools(): string[] {
return Array.from(this.tools.keys());
}
getTool(name: string): Tool | undefined {
return this.tools.get(name);
}
getDefaultAccountId(): string | undefined {
return this.defaultAccountId;
}
}
// Main entry point
if (require.main === module) {
const server = new NewRelicMCPServer();
server.start().catch(console.error);
}