index.ts•4.44 kB
#!/usr/bin/env node
// Copyright (c) 2025 Nulab inc.
// Licensed under the MIT License.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import * as backlogjs from 'backlog-js';
import dotenv from 'dotenv';
import { default as env } from 'env-var';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { createTranslationHelper } from './createTranslationHelper.js';
import { registerDyamicTools, registerTools } from './registerTools.js';
import { dynamicTools } from './tools/dynamicTools/toolsets.js';
import { logger } from './utils/logger.js';
import { createToolRegistrar } from './utils/toolRegistrar.js';
import { buildToolsetGroup } from './utils/toolsetUtils.js';
import { wrapServerWithToolRegistry } from './utils/wrapServerWithToolRegistry.js';
import { VERSION } from './version.js';
dotenv.config();
const domain = env.get('BACKLOG_DOMAIN').required().asString();
const apiKey = env.get('BACKLOG_API_KEY').required().asString();
const backlog = new backlogjs.Backlog({ host: domain, apiKey: apiKey });
const argv = yargs(hideBin(process.argv))
.option('max-tokens', {
type: 'number',
describe: 'Maximum number of tokens allowed in the response',
default: env.get('MAX_TOKENS').default('50000').asIntPositive(),
})
.option('optimize-response', {
type: 'boolean',
describe:
'Enable GraphQL-style response optimization to include only requested fields',
default: env.get('OPTIMIZE_RESPONSE').default('false').asBool(),
})
.option('prefix', {
type: 'string',
describe: 'Optional string prefix to prepend to all generated outputs',
default: env.get('PREFIX').default('').asString(),
})
.option('export-translations', {
type: 'boolean',
describe: 'Export translations and exit',
default: false,
})
.option('enable-toolsets', {
type: 'array',
describe: `Specify which toolsets to enable. Defaults to 'all'.
Available toolsets:
- space: Tools for managing Backlog space settings and general information
- project: Tools for managing projects, categories, custom fields, and issue types
- issue: Tools for managing issues and their comments
- wiki: Tools for managing wiki pages
- git: Tools for managing Git repositories and pull requests
- notifications: Tools for managing user notifications`,
default: env.get('ENABLE_TOOLSETS').default('all').asArray(','),
})
.option('dynamic-toolsets', {
type: 'boolean',
describe:
'Enable dynamic toolsets such as enable_toolset, list_available_toolsets, etc.',
default: env.get('ENABLE_DYNAMIC_TOOLSETS').default('false').asBool(),
})
.parseSync();
const useFields = argv.optimizeResponse;
const server = wrapServerWithToolRegistry(
new McpServer({
name: 'backlog',
description: useFields
? `You can include only the fields you need using GraphQL-style syntax.
Start with the example above and customize freely.`
: undefined,
version: VERSION,
})
);
const transHelper = createTranslationHelper();
const maxTokens = argv.maxTokens;
const prefix = argv.prefix;
let enabledToolsets = argv.enableToolsets as string[];
// If dynamic toolsets are enabled, remove "all" to allow for selective enabling via commands
if (argv.dynamicToolsets) {
enabledToolsets = enabledToolsets.filter((a) => a != 'all');
}
const mcpOption = { useFields: useFields, maxTokens, prefix };
const toolsetGroup = buildToolsetGroup(backlog, transHelper, enabledToolsets);
// Register all tools
registerTools(server, toolsetGroup, mcpOption);
// Register dynamic tool management tools if enabled
if (argv.dynamicToolsets) {
const registrar = createToolRegistrar(server, toolsetGroup, mcpOption);
const dynamicToolsetGroup = dynamicTools(
registrar,
transHelper,
toolsetGroup
);
registerDyamicTools(server, dynamicToolsetGroup, prefix);
}
if (argv.exportTranslations) {
const data = transHelper.dump();
// eslint-disable-next-line no-console
console.log(JSON.stringify(data, null, 2));
process.exit(0);
}
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
logger.info('Backlog MCP Server running on stdio');
}
main().catch((error) => {
logger.error({ err: error }, 'Fatal error in main()');
process.exit(1);
});