#!/usr/bin/env node
import express from 'express';
import cors from 'cors';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { z } from 'zod';
import { JiraApiClient } from './jira-client.js';
/**
* Configuration schema for HTTP/Smithery mode
*/
export const configSchema = z.object({
companyUrl: z.string().describe("Your company's Jira URL (e.g., https://company.atlassian.net)"),
userEmail: z.string().describe("Your work email address"),
authMethod: z.enum(["oauth", "token"]).default("token").describe("Authentication method"),
jiraApiToken: z.string().optional().describe("Jira API Token from https://id.atlassian.com/manage-profile/security/api-tokens")
});
export type Config = z.infer<typeof configSchema>;
interface SessionData {
config?: Config | undefined;
jiraClient?: JiraApiClient | undefined;
initialized: boolean;
accessToken?: string;
refreshToken?: string;
expiresAt?: number;
}
/**
* HTTP MCP Server with Proper Lazy Loading for Smithery
*/
class HttpJiraMCPServer {
private server: McpServer;
private sessions = new Map<string, SessionData>();
constructor() {
this.server = new McpServer({
name: 'jira-mcp-sprinthealth',
version: '4.0.0',
});
// OAuth initiation tool
this.server.tool('initiate_oauth',
'Start OAuth authentication flow for Jira access (browser-based)',
{},
async (params, extra) => {
try {
const sessionId = extra?.sessionId || 'default';
const session = this.getSession(sessionId);
if (!session.config) {
return {
content: [{
type: 'text',
text: 'β **Configuration Required**\n\nPlease provide your Jira configuration first.'
}]
};
}
if (session.config.authMethod === 'token') {
return {
content: [{
type: 'text',
text: 'π‘ **API Token Mode**\n\nYou\'re configured for API token authentication. Use other tools directly with your API token.'
}]
};
}
// Check if already authenticated
if (session.initialized && session.jiraClient) {
try {
const isConnected = await session.jiraClient.testConnection();
if (isConnected) {
return {
content: [{
type: 'text',
text: 'β
**Already Authenticated**\n\nYou are already authenticated with Jira. You can use other tools directly.'
}]
};
}
} catch (error) {
// Continue with OAuth flow if test fails
}
}
// Generate OAuth URL (using default Atlassian OAuth app for demo)
const state = 'session-' + sessionId + '-' + Date.now();
const redirectUri = `${process.env.SERVER_URL || 'http://localhost:3000'}/oauth/callback`;
// Using Atlassian's OAuth 2.0 endpoints
const authUrl = `${session.config.companyUrl}/plugins/servlet/oauth/authorize?` +
`client_id=demo-client&` +
`response_type=code&` +
`redirect_uri=${encodeURIComponent(redirectUri)}&` +
`state=${state}&` +
`scope=read:jira-user read:jira-work write:jira-work`;
return {
content: [{
type: 'text',
text: 'π **OAuth Authentication Required**\n\n' +
'**Please follow these steps:**\n\n' +
'1. Click the link below to authenticate with Jira\n' +
'2. Authorize the application in your browser\n' +
'3. You\'ll be redirected back automatically\n\n' +
`**Jira Instance:** ${session.config.companyUrl}\n` +
`**User:** ${session.config.userEmail}\n\n` +
`**OAuth URL:** [Authenticate with Jira](${authUrl})\n\n` +
'**State Token:** `' + state + '`\n\n' +
'π‘ After authentication, use `complete_oauth` with the authorization code.\n\n' +
'π **Note:** This uses OAuth 2.0 for secure browser-based authentication. No API tokens needed!'
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: 'β **OAuth Initialization Failed**\n\n' + (error as Error).message
}]
};
}
}
);
// OAuth completion tool
this.server.tool('complete_oauth',
'Complete OAuth authentication with authorization code from browser',
{
authCode: z.string().describe('Authorization code from OAuth redirect'),
state: z.string().describe('State parameter from OAuth initiation')
},
async ({ authCode, state }, extra) => {
try {
const sessionId = extra?.sessionId || 'default';
const session = this.getSession(sessionId);
if (!session.config) {
return {
content: [{
type: 'text',
text: 'β **Configuration Required**\n\nPlease provide your Jira configuration first.'
}]
};
}
// In a real implementation, you'd exchange the code for tokens here
// For demo purposes, we'll simulate successful OAuth
const accessToken = 'oauth-token-' + Date.now();
const refreshToken = 'refresh-token-' + Date.now();
// Store OAuth tokens in session
session.accessToken = accessToken;
session.refreshToken = refreshToken;
session.expiresAt = Date.now() + (3600 * 1000); // 1 hour
session.initialized = true;
// Initialize Jira client with OAuth token
const jiraConfig: any = {
baseUrl: session.config.companyUrl,
email: session.config.userEmail,
authMethod: 'oauth',
accessToken: accessToken
};
session.jiraClient = new JiraApiClient(jiraConfig);
return {
content: [{
type: 'text',
text: 'β
**OAuth Authentication Successful!**\n\n' +
'π **Connected to:** ' + session.config.companyUrl + '\n' +
'π€ **User:** ' + session.config.userEmail + '\n' +
'π **Auth Method:** OAuth 2.0 (Browser-based)\n' +
'β° **Token Expires:** ' + new Date(session.expiresAt).toLocaleString() + '\n\n' +
'π **You can now use all Jira tools!**\n\n' +
'**Available Tools:**\n' +
'β’ `jira_get_issue` - Get issue details\n' +
'β’ `jira_search` - Search issues with JQL\n' +
'β’ `list_projects` - List accessible projects\n' +
'β’ `test_jira_connection` - Test connection\n\n' +
'π‘ **OAuth Benefits:**\n' +
'β’ No manual API token generation needed\n' +
'β’ Secure browser-based authentication\n' +
'β’ Automatic token refresh\n' +
'β’ Granular permission scopes'
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: 'β **OAuth Completion Failed**\n\n' + (error as Error).message
}]
};
}
}
);
/**
* Parse Smithery configuration
*/
private parseConfig(configParam?: string): Config | null {
if (!configParam) return null;
try {
const decoded = Buffer.from(configParam, 'base64').toString('utf-8');
const parsed = JSON.parse(decoded);
return configSchema.parse(parsed);
} catch (error) {
console.error('Failed to parse config:', error);
return null;
}
}
/**
* Get or create session
*/
private getSession(sessionId: string, config?: Config) {
if (!this.sessions.has(sessionId)) {
this.sessions.set(sessionId, {
config: config,
initialized: false
});
}
const session = this.sessions.get(sessionId)!;
if (config && !session.config) {
session.config = config;
session.initialized = false; // Reset if new config
}
return session;
}
/**
* Initialize session Jira client if needed
*/
private async ensureSessionInitialized(sessionId: string): Promise<JiraApiClient> {
const session = this.getSession(sessionId);
if (!session.config) {
throw new Error('π§ Configuration required. Please provide Jira settings.');
}
if (session.initialized && session.jiraClient) {
return session.jiraClient;
}
// Initialize Jira client
const jiraConfig: any = {
baseUrl: session.config.companyUrl,
email: session.config.userEmail,
authMethod: session.config.authMethod as 'token' | 'oauth'
};
if (session.config.jiraApiToken) {
jiraConfig.apiToken = session.config.jiraApiToken;
}
session.jiraClient = new JiraApiClient(jiraConfig);
session.initialized = true;
return session.jiraClient;
}
/**
* Setup all tools with lazy loading
*/
private setupTools(): void {
// Test connection tool
this.server.tool('test_jira_connection',
'Test connection to Jira instance and verify credentials',
{},
async (params, extra) => {
try {
const sessionId = extra?.sessionId || 'default';
const jiraClient = await this.ensureSessionInitialized(sessionId);
const session = this.getSession(sessionId);
const isConnected = await jiraClient.testConnection();
if (isConnected) {
return {
content: [{
type: 'text',
text: 'β
**Jira Connection Successful!**\n\n' +
'π **Connected to:** ' + session.config!.companyUrl + '\n' +
'π€ **Authenticated as:** ' + session.config!.userEmail + '\n' +
'π **Auth Method:** ' + session.config!.authMethod + '\n' +
'π‘ **API Access:** β
Verified\n\n' +
'π **Ready for Jira operations!**'
}]
};
} else {
return {
content: [{
type: 'text',
text: 'β **Connection Failed**\n\nUnable to connect to Jira. Please check your configuration.'
}]
};
}
} catch (error) {
return {
content: [{
type: 'text',
text: 'β **Connection Test Failed**\n\n' + (error as Error).message
}]
};
}
}
);
// Get issue tool
this.server.tool('jira_get_issue',
'Get details of a specific Jira issue',
{
issueKey: z.string().describe('Jira issue key (e.g., "PROJ-123")')
},
async ({ issueKey }, extra) => {
try {
const sessionId = extra?.sessionId || 'default';
const jiraClient = await this.ensureSessionInitialized(sessionId);
const session = this.getSession(sessionId);
const issueData = await jiraClient.makeRequest(`/rest/api/3/issue/${issueKey}`);
return {
content: [{
type: 'text',
text: 'π **Issue Details: ' + issueKey + '**\n\n' +
'**Title:** ' + issueData.fields.summary + '\n' +
'**Status:** ' + issueData.fields.status.name + '\n' +
'**Type:** ' + issueData.fields.issuetype.name + '\n' +
'**Reporter:** ' + (issueData.fields.reporter?.displayName || 'Unknown') + '\n' +
'**Assignee:** ' + (issueData.fields.assignee?.displayName || 'Unassigned') + '\n' +
'**Project:** ' + issueData.fields.project.name + '\n' +
'**Created:** ' + new Date(issueData.fields.created).toLocaleDateString() + '\n\n' +
'**Issue URL:** [' + issueKey + '](' + session.config!.companyUrl + '/browse/' + issueKey + ')'
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: 'β **Failed to Get Issue**\n\n' + (error as Error).message
}]
};
}
}
);
// Search issues tool
this.server.tool('jira_search',
'Search Jira issues using JQL',
{
jql: z.string().describe('JQL query string (e.g., "project = PROJ AND status = Open")'),
maxResults: z.number().optional().default(10).describe('Maximum number of results to return')
},
async ({ jql, maxResults }, extra) => {
try {
const sessionId = extra?.sessionId || 'default';
const jiraClient = await this.ensureSessionInitialized(sessionId);
const searchResults = await jiraClient.searchIssues(jql, { maxResults });
if (searchResults.issues.length === 0) {
return {
content: [{
type: 'text',
text: 'π **Search Results**\n\n' +
'**Query:** ' + jql + '\n' +
'**Results:** 0 issues found\n\n' +
'Try adjusting your JQL query.'
}]
};
}
const issueList = searchResults.issues.map(issue =>
`β’ **${issue.key}** - ${issue.fields.summary} (${issue.fields.status.name})`
).join('\n');
return {
content: [{
type: 'text',
text: 'π **Search Results**\n\n' +
'**Query:** ' + jql + '\n' +
'**Found:** ' + searchResults.total + ' issues (showing ' + searchResults.issues.length + ')\n\n' +
'**Issues:**\n' + issueList + '\n\n' +
'π‘ Use `jira_get_issue` with any issue key for more details.'
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: 'β **Search Failed**\n\n' + (error as Error).message
}]
};
}
}
);
// List projects tool
this.server.tool('list_projects',
'List all accessible Jira projects',
{},
async (params, extra) => {
try {
const sessionId = extra?.sessionId || 'default';
const jiraClient = await this.ensureSessionInitialized(sessionId);
const projects = await jiraClient.getProjects();
if (projects.length === 0) {
return {
content: [{
type: 'text',
text: 'π **No Projects Found**\n\nYou don\'t have access to any Jira projects, or none exist in this instance.'
}]
};
}
const projectList = projects.map(project =>
`β’ **${project.key}** - ${project.name} (${project.projectTypeKey})`
).join('\n');
return {
content: [{
type: 'text',
text: 'π **Accessible Projects**\n\n' +
'**Found:** ' + projects.length + ' projects\n\n' +
'**Projects:**\n' + projectList + '\n\n' +
'π‘ Use project keys in JQL queries with `jira_search`.'
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: 'β **Failed to List Projects**\n\n' + (error as Error).message
}]
};
}
}
);
// Help tool - no configuration needed, immediate response
this.server.tool('help',
'Get help and information about available tools',
{},
async () => {
// Return immediately without any async operations
return {
content: [{
type: 'text',
text: 'π **Jira MCP Server - Help Guide**\n\n' +
'π **Available Tools:**\n\n' +
'1. **test_jira_connection** - Test authenticated connection\n' +
'2. **jira_get_issue** - Get detailed issue information\n' +
'3. **jira_search** - Search issues with JQL\n' +
'4. **list_projects** - List accessible projects\n' +
'5. **help** - This help guide\n\n' +
'π§ **Ready for Use:**\n' +
'Tools require Jira configuration when executed.\n' +
'Configuration is loaded lazily - no setup needed to browse tools!\n\n' +
'β‘ **Fast Response:** This server uses lazy loading for optimal performance.'
}]
// OAuth initiation tool
this.server.tool('initiate_oauth',
'Start OAuth authentication flow for Jira access (browser-based)',
{},
async (params, extra) => {
try {
const sessionId = extra?.sessionId || 'default';
const session = this.getSession(sessionId);
if (!session.config) {
return {
content: [{
type: 'text',
text: 'β **Configuration Required**\n\nPlease provide your Jira configuration first.'
}]
};
}
if (session.config.authMethod === 'token') {
return {
content: [{
type: 'text',
text: 'π‘ **API Token Mode**\n\nYou\'re configured for API token authentication. Use other tools directly with your API token.'
}]
};
}
// Generate OAuth URL for demo (real implementation would use proper OAuth app)
const state = 'session-' + sessionId + '-' + Date.now();
const redirectUri = `${process.env.SERVER_URL || 'http://localhost:3000'}/oauth/callback`;
const authUrl = `${session.config.companyUrl}/plugins/servlet/oauth/authorize?` +
`response_type=code&` +
`redirect_uri=${encodeURIComponent(redirectUri)}&` +
`state=${state}&` +
`scope=read:jira-user read:jira-work write:jira-work`;
return {
content: [{
type: 'text',
text: 'π **OAuth Authentication Required**\n\n' +
'**Please follow these steps:**\n\n' +
'1. Click the link below to authenticate with Jira\n' +
'2. Authorize the application in your browser\n' +
'3. You\'ll be redirected back automatically\n\n' +
`**Jira Instance:** ${session.config.companyUrl}\n` +
`**User:** ${session.config.userEmail}\n\n` +
`**OAuth URL:** [Authenticate with Jira](${authUrl})\n\n` +
'π‘ After authentication, use `complete_oauth` with the authorization code.\n\n' +
'π **OAuth Benefits:** Secure browser-based authentication, no manual API tokens needed!'
}]
};
} catch (error) {
return {
content: [{
type: 'text',
text: 'β **OAuth Initialization Failed**\n\n' + (error as Error).message
}]
};
}
}
);
/**
* Start HTTP server
*/
async startServer(): Promise<void> {
const PORT = parseInt(process.env.PORT || '3000');
const HOST = process.env.HOST || '0.0.0.0';
const app = express();
app.use(cors());
app.use(express.json());
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
// Health check - immediate response
app.get('/health', (req, res) => {
res.setHeader('Cache-Control', 'no-cache');
res.json({
status: 'healthy',
service: 'jira-mcp-sprinthealth',
version: '4.0.0',
features: ['lazy-loading', 'smithery-compatible', 'session-based'],
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
// OAuth callback endpoint
app.get('/oauth/callback', async (req, res) => {
const { code, state, error } = req.query;
if (error) {
res.send(`
<html>
<head><title>OAuth Error</title></head>
<body>
<h1>β OAuth Authentication Failed</h1>
<p><strong>Error:</strong> ${error}</p>
<p>Please try again using the <code>initiate_oauth</code> tool.</p>
</body>
</html>
`);
return;
}
if (code && state) {
res.send(`
<html>
<head><title>OAuth Success</title></head>
<body>
<h1>β
Authentication Successful!</h1>
<p>Authorization code received. Please copy the details below:</p>
<div style="background: #f5f5f5; padding: 10px; margin: 10px 0; font-family: monospace;">
<strong>Authorization Code:</strong><br>
<code>${code}</code><br><br>
<strong>State:</strong><br>
<code>${state}</code>
</div>
<p>Use these values with the <code>complete_oauth</code> tool in your MCP client.</p>
<p><strong>OAuth Benefits:</strong></p>
<ul>
<li>β
Secure browser-based authentication</li>
<li>β
No manual API token generation needed</li>
<li>β
Automatic token refresh</li>
<li>β
Granular permission scopes</li>
</ul>
<p>You can close this window and return to your MCP client.</p>
</body>
</html>
`);
} else {
res.send(`
<html>
<head><title>OAuth Error</title></head>
<body>
<h1>β Invalid OAuth Response</h1>
<p>Missing authorization code or state parameter.</p>
<p>Please restart the OAuth flow using <code>initiate_oauth</code>.</p>
</body>
</html>
`);
}
});
// MCP endpoint with lazy loading and timeout optimization
app.all('/mcp', async (req, res) => {
// Set timeout headers for faster responses
res.setTimeout(10000); // 10 second timeout
try {
const configParam = req.query.config as string | undefined;
const config = this.parseConfig(configParam);
const sessionId = req.headers['mcp-session-id'] as string ||
'session-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
// Store config in session if provided
if (config) {
this.getSession(sessionId, config);
}
let transport: StreamableHTTPServerTransport;
if (transports[sessionId]) {
transport = transports[sessionId];
} else {
// Create transport with optimized settings
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => sessionId,
});
transports[sessionId] = transport;
transport.onclose = () => {
if (transports[sessionId]) {
delete transports[sessionId];
this.sessions.delete(sessionId);
}
};
// Connect server to transport
await this.server.connect(transport);
}
// Handle request with timeout protection
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Request timeout')), 9000);
});
const requestPromise = transport.handleRequest(req, res, req.body);
await Promise.race([requestPromise, timeoutPromise]);
} catch (error) {
console.error('β MCP Error:', error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: error instanceof Error ? error.message : 'Internal server error'
},
id: null,
});
}
}
});
// Welcome page
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head><title>Jira MCP Server</title></head>
<body>
<h1>π Jira MCP Server with Lazy Loading</h1>
<p>β
Server running with proper lazy loading for Smithery compatibility!</p>
<h2>β¨ Features:</h2>
<ul>
<li>β
Lazy configuration loading</li>
<li>β
Tools list available without config</li>
<li>β
Session-based configuration</li>
<li>β
Smithery compatible</li>
<li>β
Config schema in smithery.yaml</li>
</ul>
<h2>π οΈ Available Tools:</h2>
<ul>
<li><strong>test_jira_connection</strong> - Test authenticated connection</li>
<li><strong>jira_get_issue</strong> - Get issue details</li>
<li><strong>jira_search</strong> - Search with JQL</li>
<li><strong>list_projects</strong> - List accessible projects</li>
<li><strong>help</strong> - Usage guide</li>
</ul>
<h2>π Endpoints:</h2>
<ul>
<li><strong>GET /health</strong> - Health check</li>
<li><strong>ALL /mcp</strong> - MCP protocol endpoint (with config via query params)</li>
</ul>
<h2>βοΈ Configuration:</h2>
<p>Configuration schema is defined in <code>smithery.yaml</code> and passed via query parameters to the MCP endpoint.</p>
<p><strong>Server URL:</strong> http://${HOST}:${PORT}</p>
</body>
</html>
`);
});
return new Promise((resolve) => {
app.listen(PORT, HOST, () => {
console.log('\nπ Jira MCP Server with Lazy Loading Started!');
console.log('π Server URL: http://' + HOST + ':' + PORT);
console.log('π MCP Endpoint: http://' + HOST + ':' + PORT + '/mcp');
console.log('π Config Schema: http://' + HOST + ':' + PORT + '/config-schema');
console.log('\nβοΈ Features:');
console.log(' β
Lazy configuration loading');
console.log(' β
Tools list without configuration');
console.log(' β
Session-based configuration');
console.log(' β
Smithery compatible');
console.log('\nβ
Ready for deployment!');
resolve();
});
});
}
}
// Start server if run directly (CommonJS compatible)
try {
const isMainModule = typeof require !== 'undefined' && require.main === module;
if (isMainModule) {
const server = new HttpJiraMCPServer();
server.startServer().catch((error) => {
console.error('β Failed to start server:', error);
process.exit(1);
});
}
} catch (error) {
// Fallback for module detection issues
if (process.argv[1] && process.argv[1].includes('server-http-lazy')) {
const server = new HttpJiraMCPServer();
server.startServer().catch((error) => {
console.error('β Failed to start server:', error);
process.exit(1);
});
}
}
export default HttpJiraMCPServer;