Skip to content

Commit 6bbbf25

Browse files
committed
Add MCP server support for GitHub issue comments Implement MCP SDK integration to expose GitHub issue comment operations as remote procedures. Add Zod schemas for validation and express router for request handling. Fixes #77
1 parent 18d8b91 commit 6bbbf25

File tree

12 files changed

+1605
-3
lines changed

12 files changed

+1605
-3
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,16 @@
6060
"prepare": "husky"
6161
},
6262
"dependencies": {
63+
"@modelcontextprotocol/sdk": "^1.0.4",
6364
"chalk": "^5.3.0",
6465
"commander": "^11.1.0",
6566
"dotenv-flow": "^4.1.0",
6667
"execa": "^8.0.1",
6768
"fs-extra": "^11.1.1",
6869
"inquirer": "^9.2.12",
6970
"jsonc-parser": "^3.3.1",
70-
"ora": "^7.0.1"
71+
"ora": "^7.0.1",
72+
"zod": "^3.23.8"
7173
},
7274
"devDependencies": {
7375
"@eslint/js": "^9.35.0",

pnpm-lock.yaml

Lines changed: 605 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands/ignite.test.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { IgniteCommand } from './ignite.js'
33
import type { PromptTemplateManager } from '../lib/PromptTemplateManager.js'
44
import type { GitWorktreeManager } from '../lib/GitWorktreeManager.js'
55
import * as claudeUtils from '../utils/claude.js'
6+
import * as githubUtils from '../utils/github.js'
67

78
describe('IgniteCommand', () => {
89
let command: IgniteCommand
@@ -599,4 +600,138 @@ describe('IgniteCommand', () => {
599600
}
600601
})
601602
})
603+
604+
describe('MCP Configuration', () => {
605+
it('should generate MCP config for issue workflows', async () => {
606+
const launchClaudeSpy = vi.spyOn(claudeUtils, 'launchClaude').mockResolvedValue(undefined)
607+
const getRepoInfoSpy = vi.spyOn(githubUtils, 'getRepoInfo').mockResolvedValue({
608+
owner: 'testowner',
609+
name: 'testrepo',
610+
})
611+
612+
const originalCwd = process.cwd
613+
process.cwd = vi.fn().mockReturnValue('/path/to/feat/issue-77-mcp-test')
614+
615+
try {
616+
await command.execute()
617+
618+
// Verify launchClaude was called with mcpConfig
619+
const launchClaudeCall = launchClaudeSpy.mock.calls[0]
620+
expect(launchClaudeCall[1]).toHaveProperty('mcpConfig')
621+
expect(launchClaudeCall[1].mcpConfig).toBeInstanceOf(Array)
622+
expect(launchClaudeCall[1].mcpConfig.length).toBeGreaterThan(0)
623+
624+
// Verify MCP config structure
625+
const mcpConfig = launchClaudeCall[1].mcpConfig[0]
626+
expect(mcpConfig).toHaveProperty('mcpServers')
627+
expect(mcpConfig.mcpServers).toHaveProperty('github_comment')
628+
expect(mcpConfig.mcpServers.github_comment).toHaveProperty('command')
629+
expect(mcpConfig.mcpServers.github_comment).toHaveProperty('args')
630+
expect(mcpConfig.mcpServers.github_comment).toHaveProperty('env')
631+
} finally {
632+
process.cwd = originalCwd
633+
launchClaudeSpy.mockRestore()
634+
getRepoInfoSpy.mockRestore()
635+
}
636+
})
637+
638+
it('should generate MCP config for PR workflows', async () => {
639+
const launchClaudeSpy = vi.spyOn(claudeUtils, 'launchClaude').mockResolvedValue(undefined)
640+
const getRepoInfoSpy = vi.spyOn(githubUtils, 'getRepoInfo').mockResolvedValue({
641+
owner: 'testowner',
642+
name: 'testrepo',
643+
})
644+
645+
const originalCwd = process.cwd
646+
process.cwd = vi.fn().mockReturnValue('/path/to/feature_pr_456')
647+
648+
try {
649+
await command.execute()
650+
651+
// Verify launchClaude was called with mcpConfig
652+
const launchClaudeCall = launchClaudeSpy.mock.calls[0]
653+
expect(launchClaudeCall[1]).toHaveProperty('mcpConfig')
654+
expect(launchClaudeCall[1].mcpConfig).toBeInstanceOf(Array)
655+
} finally {
656+
process.cwd = originalCwd
657+
launchClaudeSpy.mockRestore()
658+
getRepoInfoSpy.mockRestore()
659+
}
660+
})
661+
662+
it('should NOT generate MCP config for regular workflows', async () => {
663+
const launchClaudeSpy = vi.spyOn(claudeUtils, 'launchClaude').mockResolvedValue(undefined)
664+
665+
const originalCwd = process.cwd
666+
process.cwd = vi.fn().mockReturnValue('/path/to/main')
667+
668+
mockGitWorktreeManager.getRepoInfo = vi.fn().mockResolvedValue({
669+
currentBranch: 'main',
670+
})
671+
672+
try {
673+
await command.execute()
674+
675+
// Verify launchClaude was NOT called with mcpConfig
676+
const launchClaudeCall = launchClaudeSpy.mock.calls[0]
677+
expect(launchClaudeCall[1].mcpConfig).toBeUndefined()
678+
} finally {
679+
process.cwd = originalCwd
680+
launchClaudeSpy.mockRestore()
681+
}
682+
})
683+
684+
it('should include correct environment variables in MCP config for issue workflows', async () => {
685+
const launchClaudeSpy = vi.spyOn(claudeUtils, 'launchClaude').mockResolvedValue(undefined)
686+
const getRepoInfoSpy = vi.spyOn(githubUtils, 'getRepoInfo').mockResolvedValue({
687+
owner: 'testowner',
688+
name: 'testrepo',
689+
})
690+
691+
const originalCwd = process.cwd
692+
process.cwd = vi.fn().mockReturnValue('/path/to/feat/issue-88-env-test')
693+
694+
try {
695+
await command.execute()
696+
697+
const launchClaudeCall = launchClaudeSpy.mock.calls[0]
698+
const mcpConfig = launchClaudeCall[1].mcpConfig[0]
699+
const env = mcpConfig.mcpServers.github_comment.env
700+
701+
expect(env).toHaveProperty('REPO_OWNER')
702+
expect(env).toHaveProperty('REPO_NAME')
703+
expect(env).toHaveProperty('GITHUB_EVENT_NAME', 'issues')
704+
expect(env).toHaveProperty('GITHUB_API_URL')
705+
} finally {
706+
process.cwd = originalCwd
707+
launchClaudeSpy.mockRestore()
708+
getRepoInfoSpy.mockRestore()
709+
}
710+
})
711+
712+
it('should include correct environment variables in MCP config for PR workflows', async () => {
713+
const launchClaudeSpy = vi.spyOn(claudeUtils, 'launchClaude').mockResolvedValue(undefined)
714+
const getRepoInfoSpy = vi.spyOn(githubUtils, 'getRepoInfo').mockResolvedValue({
715+
owner: 'testowner',
716+
name: 'testrepo',
717+
})
718+
719+
const originalCwd = process.cwd
720+
process.cwd = vi.fn().mockReturnValue('/path/to/feature_pr_789')
721+
722+
try {
723+
await command.execute()
724+
725+
const launchClaudeCall = launchClaudeSpy.mock.calls[0]
726+
const mcpConfig = launchClaudeCall[1].mcpConfig[0]
727+
const env = mcpConfig.mcpServers.github_comment.env
728+
729+
expect(env).toHaveProperty('GITHUB_EVENT_NAME', 'pull_request')
730+
} finally {
731+
process.cwd = originalCwd
732+
launchClaudeSpy.mockRestore()
733+
getRepoInfoSpy.mockRestore()
734+
}
735+
})
736+
})
602737
})

src/commands/ignite.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ClaudeWorkflowOptions } from '../lib/ClaudeService.js'
44
import { GitWorktreeManager } from '../lib/GitWorktreeManager.js'
55
import { launchClaude, ClaudeCliOptions } from '../utils/claude.js'
66
import { PromptTemplateManager, TemplateVariables } from '../lib/PromptTemplateManager.js'
7+
import { getRepoInfo } from '../utils/github.js'
78

89
/**
910
* IgniteCommand: Auto-detect workspace context and launch Claude
@@ -78,11 +79,24 @@ export class IgniteCommand {
7879
claudeOptions.branchName = context.branchName
7980
}
8081

82+
// Step 4.5: Generate MCP config for issue/PR workflows
83+
let mcpConfig: Record<string, unknown>[] | undefined
84+
if (context.type === 'issue' || context.type === 'pr') {
85+
try {
86+
mcpConfig = await this.generateMcpConfig(context)
87+
logger.debug('Generated MCP configuration for GitHub comment broker')
88+
} catch (error) {
89+
// Log warning but continue without MCP
90+
logger.warn(`Failed to generate MCP config: ${error instanceof Error ? error.message : 'Unknown error'}`)
91+
}
92+
}
93+
8194
logger.debug('Launching Claude in current terminal', {
8295
type: context.type,
8396
model,
8497
permissionMode,
8598
workspacePath: context.workspacePath,
99+
hasMcpConfig: !!mcpConfig,
86100
})
87101

88102
logger.info('✨ Launching Claude in current terminal...')
@@ -91,6 +105,7 @@ export class IgniteCommand {
91105
await launchClaude(userPrompt, {
92106
...claudeOptions,
93107
appendSystemPrompt: systemInstructions,
108+
...(mcpConfig && { mcpConfig }),
94109
})
95110
} catch (error) {
96111
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
@@ -363,4 +378,40 @@ export class IgniteCommand {
363378

364379
return port
365380
}
381+
382+
/**
383+
* Generate MCP configuration for GitHub comment broker
384+
* Returns array of MCP server config objects
385+
*/
386+
private async generateMcpConfig(context: ClaudeWorkflowOptions): Promise<Record<string, unknown>[]> {
387+
// Get repository information
388+
const repoInfo = await getRepoInfo()
389+
390+
// Determine GitHub event name based on context type
391+
const githubEventName = context.type === 'issue' ? 'issues' : 'pull_request'
392+
// const args = [path.join(path.dirname(new URL(import.meta.url).pathname), '../mcp/github-comment-server.js')]
393+
394+
// logger.debug('')
395+
// Build MCP server configuration wrapped in github_comment key
396+
const mcpServerConfig = {
397+
mcpServers: {
398+
github_comment: {
399+
transport: 'stdio',
400+
command: 'node',
401+
args: [path.join(path.dirname(new globalThis.URL(import.meta.url).pathname), '../dist/mcp/github-comment-server.js')],
402+
env: {
403+
REPO_OWNER: repoInfo.owner,
404+
REPO_NAME: repoInfo.name,
405+
GITHUB_EVENT_NAME: githubEventName,
406+
GITHUB_API_URL: 'https://api.github.com/',
407+
},
408+
},
409+
},
410+
}
411+
412+
logger.debug('Generated MCP config', { mcpServerConfig })
413+
414+
// Return as array (user clarification: mcpConfig should be an array)
415+
return [mcpServerConfig]
416+
}
366417
}

0 commit comments

Comments
 (0)