Compare commits

...

23 Commits

Author SHA1 Message Date
Sean Goedecke 334892bb20 Merge pull request #142 from maartenvandiemen/feature/pass-toolsets
Pass GitHub MCP Tools
2025-12-01 08:48:16 +11:00
Maarten van Diemen bbe0ccb244 Fix failing tests 2025-11-30 22:20:19 +01:00
Maarten van Diemen ca3b99ea74 Undo changes in tests.
Undo linter change
2025-11-30 22:14:02 +01:00
Maarten van Diemen 8a5d2ea4a1 Merge branch 'main' into feature/pass-toolsets 2025-11-30 22:08:41 +01:00
Sean Goedecke 112739fb15 Merge pull request #139 from GulerSevil/patch-1
Clarify token requirements for MCP integration
2025-12-01 05:35:40 +11:00
Sevil f95554969e Merge branch 'main' into patch-1 2025-11-29 21:47:36 +01:00
Sevil 9e60aa0a3f Lint fix
Lint fix
2025-11-29 21:15:11 +01:00
Sean Goedecke 02c6cc30ae Merge pull request #150 from actions/sgoedecke/mock-inference-in-ci
Mock inference in CI
2025-11-28 08:17:33 +11:00
Sean Goedecke 18d468666d fix: keep response-file temp file for downstream steps
The temporary file created for response-file was being cleaned up
before downstream steps could access it. Now using keep: true to
ensure the file persists until the job completes.

Also added script/ to eslint ignores for the mock server.
2025-11-27 21:06:42 +00:00
Sean Goedecke fd73d0264c Mock inference in CI 2025-11-27 20:59:41 +00:00
Maarten van Diemen 27350b2a98 Merge branch 'feature/pass-toolsets' of https://github.com/maartenvandiemen/ai-inference into feature/pass-toolsets 2025-11-26 22:58:36 +01:00
Maarten van Diemen e8987e92e0 Fix linter
Update GitHub Actions
2025-11-26 22:58:31 +01:00
Sevil 2d03946378 Merge branch 'main' into patch-1 2025-11-25 15:01:12 +01:00
Sevil d061fc5469 Update README.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-25 14:59:33 +01:00
Maarten van Diemen 2d2f67ec42 Update CI workflow to trigger on pull requests and pushes 2025-11-24 13:22:20 +01:00
Maarten van Diemen 9170087739 Update CI workflow to use manual trigger only
Removed automatic triggers for pull requests and pushes to main branch.
2025-11-24 13:19:46 +01:00
Maarten van Diemen 62db90ab13 Upgrade checkout to V6 2025-11-24 13:16:10 +01:00
Maarten van Diemen 16f2d5c46b Merge with main 2025-11-24 13:12:28 +01:00
Maarten van Diemen 95443f8d18 Merge with main 2025-11-24 13:06:01 +01:00
Sean Goedecke 48758ceaff Merge branch 'main' into feature/pass-toolsets 2025-11-24 10:19:55 +11:00
Maarten van Diemen 4b4b2e8afe build index.js 2025-11-02 23:37:23 +01:00
Maarten van Diemen 932a853db4 Initial implementation for passing toolsets 2025-11-02 23:20:24 +01:00
Sevil e0da58c63f Clarify token requirements for MCP integration
Updated authentication section to clarify token usage for MCP integration.
2025-10-25 22:53:45 +02:00
15 changed files with 280 additions and 100 deletions
+3 -3
View File
@@ -28,11 +28,11 @@ jobs:
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Node.js
id: setup-node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version-file: .node-version
cache: npm
@@ -66,7 +66,7 @@ jobs:
- if: ${{ failure() && steps.diff.outcome == 'failure' }}
name: Upload Artifact
id: upload
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: dist
path: dist/
+75 -8
View File
@@ -20,11 +20,11 @@ jobs:
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Node.js
id: setup-node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version-file: .node-version
cache: npm
@@ -54,22 +54,53 @@ jobs:
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: .node-version
- name: Start Mock Inference Server
id: mock-server
run: |
node script/mock-inference-server.mjs &
echo "pid=$!" >> $GITHUB_OUTPUT
# Wait for server to be ready
for i in {1..10}; do
if curl -s http://localhost:3456/health > /dev/null; then
echo "Mock server is ready"
break
fi
sleep 1
done
- name: Test Local Action
id: test-action
continue-on-error: true
uses: ./
with:
prompt: hello
endpoint: http://localhost:3456
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Print Output
id: output
continue-on-error: true
run: echo "${{ steps.test-action.outputs.response }}"
- name: Verify Output
run: |
response="${{ steps.test-action.outputs.response }}"
if [[ -z "$response" ]]; then
echo "Error: No response received"
exit 1
fi
echo "Response received: $response"
- name: Stop Mock Server
if: always()
run: kill ${{ steps.mock-server.outputs.pid }} || true
test-action-prompt-file:
name: GitHub Actions Test with Prompt File
runs-on: ubuntu-latest
@@ -77,7 +108,26 @@ jobs:
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: .node-version
- name: Start Mock Inference Server
id: mock-server
run: |
node script/mock-inference-server.mjs &
echo "pid=$!" >> $GITHUB_OUTPUT
# Wait for server to be ready
for i in {1..10}; do
if curl -s http://localhost:3456/health > /dev/null; then
echo "Mock server is ready"
break
fi
sleep 1
done
- name: Create Prompt File
run: echo "hello" > prompt.txt
@@ -87,16 +137,33 @@ jobs:
- name: Test Local Action with Prompt File
id: test-action-prompt-file
continue-on-error: true
uses: ./
with:
prompt-file: prompt.txt
system-prompt-file: system-prompt.txt
endpoint: http://localhost:3456
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Print Output
continue-on-error: true
run: |
echo "Response saved to: ${{ steps.test-action-prompt-file.outputs.response-file }}"
cat "${{ steps.test-action-prompt-file.outputs.response-file }}"
- name: Verify Output
run: |
response_file="${{ steps.test-action-prompt-file.outputs.response-file }}"
if [[ ! -f "$response_file" ]]; then
echo "Error: Response file not found"
exit 1
fi
content=$(cat "$response_file")
if [[ -z "$content" ]]; then
echo "Error: Response file is empty"
exit 1
fi
echo "Response file content: $content"
- name: Stop Mock Server
if: always()
run: kill ${{ steps.mock-server.outputs.pid }} || true
+4 -4
View File
@@ -30,19 +30,19 @@ jobs:
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Initialize CodeQL
id: initialize
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
source-root: src
- name: Autobuild
id: autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
id: analyze
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
+3 -3
View File
@@ -27,11 +27,11 @@ jobs:
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Node.js
id: setup-node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version-file: .node-version
cache: npm
@@ -42,7 +42,7 @@ jobs:
- name: Setup Ruby
id: setup-ruby
uses: ruby/setup-ruby@829114fc20da43a41d27359103ec7a63020954d4
uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71
with:
ruby-version: ruby
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
+34 -5
View File
@@ -162,8 +162,18 @@ This action now supports **read-only** integration with the GitHub-hosted Model
Context Protocol (MCP) server, which provides access to GitHub tools like
repository management, issue tracking, and pull request operations.
> [!NOTE]
> The GitHub MCP integration requires a Personal Access Token (PAT) and cannot use the built-in `GITHUB_TOKEN`.
#### Authentication
You can authenticate the MCP server with **either**:
1. **Personal Access Token (PAT)** user-scoped token
2. **GitHub App Installation Token** (`ghs_…`) short-lived, app-scoped token
> The built-in `GITHUB_TOKEN` is **not** accepted by the MCP server.
> Using a **GitHub App installation token** is recommended in most CI environments because it is short-lived and least-privilege by design.
#### Enabling MCP in the action
Set `enable-github-mcp: true` and provide a token via `github-mcp-token`.
```yaml
steps:
@@ -173,7 +183,7 @@ steps:
with:
prompt: 'List my open pull requests and create a summary'
enable-github-mcp: true
token: ${{ secrets.USER_PAT }}
token: ${{ secrets.USER_PAT }} # or a ghs_ installation token
```
If you want, you can use separate tokens for the AI inference endpoint
@@ -188,9 +198,28 @@ steps:
prompt: 'List my open pull requests and create a summary'
enable-github-mcp: true
token: ${{ secrets.GITHUB_TOKEN }}
github-mcp-token: ${{ secrets.USER_PAT }}
github-mcp-token: ${{ secrets.USER_PAT }} # or a ghs_ installation token
```
#### Configuring GitHub MCP Toolsets
By default, the GitHub MCP server provides a standard set of tools (`context`, `repos`, `issues`, `pull_requests`, `users`). You can customize which toolsets are available by specifying the `github-mcp-toolsets` parameter:
```yaml
steps:
- name: AI Inference with Custom Toolsets
id: inference
uses: actions/ai-inference@v2
with:
prompt: 'Analyze recent workflow runs and check security alerts'
enable-github-mcp: true
token: ${{ secrets.USER_PAT }}
github-mcp-toolsets: 'repos,issues,pull_requests,actions,code_security'
```
**Available toolsets:**
See: [Tool configuration](https://github.com/github/github-mcp-server/blob/main/README.md#tool-configuration)
When MCP is enabled, the AI model will have access to GitHub tools and can
perform actions like searching issues and PRs.
@@ -212,7 +241,7 @@ the action:
| `endpoint` | The endpoint to use for inference. If you're running this as part of an org, you should probably use the org-specific Models endpoint | `https://models.github.ai/inference` |
| `max-tokens` | The max number of tokens to generate | 200 |
| `enable-github-mcp` | Enable Model Context Protocol integration with GitHub tools | `false` |
| `github-mcp-token` | Token to use for GitHub MCP server (defaults to the main token if not specified). This must be a PAT in order for MCP to work | `""` |
| `github-mcp-token` | Token to use for GitHub MCP server (defaults to the main token if not specified). | `""` |
## Outputs
+6 -28
View File
@@ -75,17 +75,13 @@ vi.mock('fs', () => ({
writeFileSync: mockWriteFileSync,
}))
// Mocks for tmp module to control temporary file creation and cleanup
const mockRemoveCallback = vi.fn()
// Mocks for tmp module to control temporary file creation
const mockFileSync = vi.fn().mockReturnValue({
name: '/secure/temp/dir/modelResponse-abc123.txt',
removeCallback: mockRemoveCallback,
})
const mockSetGracefulCleanup = vi.fn()
vi.mock('tmp', () => ({
fileSync: mockFileSync,
setGracefulCleanup: mockSetGracefulCleanup,
}))
// Mock MCP and inference modules
@@ -199,7 +195,7 @@ describe('main.ts', () => {
await run()
expect(mockConnectToGitHubMCP).toHaveBeenCalledWith('fake-token')
expect(mockConnectToGitHubMCP).toHaveBeenCalledWith('fake-token', '')
expect(mockMcpInference).toHaveBeenCalledWith(
expect.objectContaining({
messages: [
@@ -226,7 +222,7 @@ describe('main.ts', () => {
await run()
expect(mockConnectToGitHubMCP).toHaveBeenCalledWith('fake-token')
expect(mockConnectToGitHubMCP).toHaveBeenCalledWith('fake-token', '')
expect(mockSimpleInference).toHaveBeenCalled()
expect(mockMcpInference).not.toHaveBeenCalled()
expect(core.warning).toHaveBeenCalledWith('MCP connection failed, falling back to simple inference')
@@ -283,7 +279,7 @@ describe('main.ts', () => {
expect(mockProcessExit).toHaveBeenCalledWith(1)
})
it('creates secure temporary files with proper cleanup', async () => {
it('creates temporary files that persist for downstream steps', async () => {
mockInputs({
prompt: 'Test prompt',
'system-prompt': 'You are a test assistant.',
@@ -291,34 +287,16 @@ describe('main.ts', () => {
await run()
expect(mockSetGracefulCleanup).toHaveBeenCalledOnce()
// Verify temp file is created with keep: true so it persists
expect(mockFileSync).toHaveBeenCalledWith({
prefix: 'modelResponse-',
postfix: '.txt',
keep: true,
})
expect(core.setOutput).toHaveBeenNthCalledWith(2, 'response-file', '/secure/temp/dir/modelResponse-abc123.txt')
expect(mockWriteFileSync).toHaveBeenCalledWith('/secure/temp/dir/modelResponse-abc123.txt', 'Hello, user!', 'utf-8')
expect(mockRemoveCallback).toHaveBeenCalledOnce()
expect(mockProcessExit).toHaveBeenCalledWith(0)
})
it('handles cleanup errors gracefully', async () => {
mockRemoveCallback.mockImplementationOnce(() => {
throw new Error('Cleanup failed')
})
mockInputs({
prompt: 'Test prompt',
'system-prompt': 'You are a test assistant.',
})
await run()
expect(mockRemoveCallback).toHaveBeenCalledOnce()
expect(core.warning).toHaveBeenCalledWith('Failed to cleanup temporary file: Error: Cleanup failed')
expect(mockProcessExit).toHaveBeenCalledWith(0)
})
})
+34
View File
@@ -113,6 +113,40 @@ describe('mcp.ts', () => {
expect(result?.tools).toHaveLength(0)
expect(core.info).toHaveBeenCalledWith('Retrieved 0 tools from GitHub MCP server')
})
it('uses default toolsets when toolsets parameter is not provided', async () => {
const token = 'test-token'
mockConnect.mockResolvedValue(undefined)
mockListTools.mockResolvedValue({tools: []})
await connectToGitHubMCP(token)
expect(core.info).toHaveBeenCalledWith('Using default GitHub MCP toolsets')
})
it('uses custom toolsets when toolsets parameter is provided', async () => {
const token = 'test-token'
const toolsets = 'repos,issues,pull_requests,actions'
mockConnect.mockResolvedValue(undefined)
mockListTools.mockResolvedValue({tools: []})
await connectToGitHubMCP(token, toolsets)
expect(core.info).toHaveBeenCalledWith('Using GitHub MCP toolsets: repos,issues,pull_requests,actions')
})
it('ignores empty toolsets parameter', async () => {
const token = 'test-token'
mockConnect.mockResolvedValue(undefined)
mockListTools.mockResolvedValue({tools: []})
await connectToGitHubMCP(token, ' ')
expect(core.info).toHaveBeenCalledWith('Using default GitHub MCP toolsets')
})
})
describe('executeToolCall', () => {
+4
View File
@@ -58,6 +58,10 @@ inputs:
description: The token to use for GitHub MCP server (defaults to the main token if not specified). This must be a PAT for MCP to work.
required: false
default: ''
github-mcp-toolsets:
description: 'Comma-separated list of toolsets to enable for GitHub MCP (e.g., "repos,issues,pull_requests,actions"). Use "all" for all toolsets, "default" for default set. If not specified, uses default toolsets (context,repos,issues,pull_requests,users).'
required: false
default: ''
# Define your outputs here.
outputs:
Generated Vendored
+21 -23
View File
@@ -42717,15 +42717,24 @@ class StreamableHTTPClientTransport {
/**
* Connect to the GitHub MCP server and retrieve available tools
*/
async function connectToGitHubMCP(token) {
async function connectToGitHubMCP(token, toolsets) {
const githubMcpUrl = 'https://api.githubcopilot.com/mcp/';
coreExports.info('Connecting to GitHub MCP server...');
const headers = {
Authorization: `Bearer ${token}`,
'X-MCP-Readonly': 'true',
};
// Add toolsets header if specified
if (toolsets && toolsets.trim() !== '') {
headers['X-MCP-Toolsets'] = toolsets;
coreExports.info(`Using GitHub MCP toolsets: ${toolsets}`);
}
else {
coreExports.info('Using default GitHub MCP toolsets');
}
const transport = new StreamableHTTPClientTransport(new URL(githubMcpUrl), {
requestInit: {
headers: {
Authorization: `Bearer ${token}`,
'X-MCP-Readonly': 'true',
},
headers,
},
});
const client = new Client({
@@ -52618,9 +52627,6 @@ function isPromptYamlFile(filePath) {
* @returns Resolves when the action is complete.
*/
async function run() {
let responseFile = null;
// Set up graceful cleanup for temporary files on process exit
tmpExports.setGracefulCleanup();
try {
const promptFilePath = coreExports.getInput('prompt-file');
const inputVariables = coreExports.getInput('input');
@@ -52656,13 +52662,14 @@ async function run() {
}
// Get GitHub MCP token (use dedicated token if provided, otherwise fall back to main token)
const githubMcpToken = coreExports.getInput('github-mcp-token') || token;
const githubMcpToolsets = coreExports.getInput('github-mcp-toolsets');
const endpoint = coreExports.getInput('endpoint');
// Build the inference request with pre-processed messages and response format
const inferenceRequest = buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, promptConfig?.modelParameters?.temperature, promptConfig?.modelParameters?.topP, maxTokens, endpoint, token);
const enableMcp = coreExports.getBooleanInput('enable-github-mcp') || false;
let modelResponse = null;
if (enableMcp) {
const mcpClient = await connectToGitHubMCP(githubMcpToken);
const mcpClient = await connectToGitHubMCP(githubMcpToken, githubMcpToolsets);
if (mcpClient) {
modelResponse = await mcpInference(inferenceRequest, mcpClient);
}
@@ -52675,10 +52682,13 @@ async function run() {
modelResponse = await simpleInference(inferenceRequest);
}
coreExports.setOutput('response', modelResponse || '');
// Create a secure temporary file instead of using the temp directory directly
responseFile = tmpExports.fileSync({
// Create a temporary file for the response that persists for downstream steps.
// We use keep: true to prevent automatic cleanup - the file will be cleaned up
// by the runner when the job completes.
const responseFile = tmpExports.fileSync({
prefix: 'modelResponse-',
postfix: '.txt',
keep: true,
});
coreExports.setOutput('response-file', responseFile.name);
if (modelResponse && modelResponse !== '') {
@@ -52695,18 +52705,6 @@ async function run() {
// Force exit to prevent hanging on open connections
process.exit(1);
}
finally {
// Explicit cleanup of temporary file if it was created
if (responseFile) {
try {
responseFile.removeCallback();
}
catch (cleanupError) {
// Log cleanup errors but don't fail the action
coreExports.warning(`Failed to cleanup temporary file: ${cleanupError}`);
}
}
}
// Force exit to prevent hanging on open connections
process.exit(0);
}
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -19,7 +19,7 @@ const compat = new FlatCompat({
export default [
{
ignores: ['**/coverage', '**/dist', '**/linter', '**/node_modules'],
ignores: ['**/coverage', '**/dist', '**/linter', '**/node_modules', 'script/**'],
},
...compat.extends(
'eslint:recommended',
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env node
/**
* A simple mock OpenAI-compatible inference server for CI testing.
* This returns predictable responses without needing real API credentials.
*/
import http from 'http'
const PORT = process.env.MOCK_SERVER_PORT || 3456
const server = http.createServer((req, res) => {
let body = ''
req.on('data', chunk => {
body += chunk.toString()
})
req.on('end', () => {
console.log(`[Mock Server] ${req.method} ${req.url}`)
// Handle chat completions endpoint
if (req.url === '/chat/completions' && req.method === 'POST') {
const request = JSON.parse(body)
const userMessage = request.messages?.find(m => m.role === 'user')?.content || 'No prompt'
const response = {
id: 'mock-completion-id',
object: 'chat.completion',
created: Date.now(),
model: request.model || 'mock-model',
choices: [
{
index: 0,
message: {
role: 'assistant',
content: `Mock response to: "${userMessage.slice(0, 50)}..."`,
},
finish_reason: 'stop',
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 20,
total_tokens: 30,
},
}
res.writeHead(200, {'Content-Type': 'application/json'})
res.end(JSON.stringify(response))
return
}
// Health check endpoint
if (req.url === '/health' || req.url === '/') {
res.writeHead(200, {'Content-Type': 'application/json'})
res.end(JSON.stringify({status: 'ok'}))
return
}
// 404 for unknown routes
res.writeHead(404, {'Content-Type': 'application/json'})
res.end(JSON.stringify({error: 'Not found'}))
})
})
server.listen(PORT, () => {
console.log(`[Mock Server] Listening on http://localhost:${PORT}`)
console.log('[Mock Server] Endpoints:')
console.log(' POST /chat/completions - Mock chat completion')
console.log(' GET /health - Health check')
})
+7 -18
View File
@@ -18,11 +18,6 @@ import {
* @returns Resolves when the action is complete.
*/
export async function run(): Promise<void> {
let responseFile: tmp.FileResult | null = null
// Set up graceful cleanup for temporary files on process exit
tmp.setGracefulCleanup()
try {
const promptFilePath = core.getInput('prompt-file')
const inputVariables = core.getInput('input')
@@ -66,6 +61,7 @@ export async function run(): Promise<void> {
// Get GitHub MCP token (use dedicated token if provided, otherwise fall back to main token)
const githubMcpToken = core.getInput('github-mcp-token') || token
const githubMcpToolsets = core.getInput('github-mcp-toolsets')
const endpoint = core.getInput('endpoint')
@@ -87,7 +83,7 @@ export async function run(): Promise<void> {
let modelResponse: string | null = null
if (enableMcp) {
const mcpClient = await connectToGitHubMCP(githubMcpToken)
const mcpClient = await connectToGitHubMCP(githubMcpToken, githubMcpToolsets)
if (mcpClient) {
modelResponse = await mcpInference(inferenceRequest, mcpClient)
@@ -101,10 +97,13 @@ export async function run(): Promise<void> {
core.setOutput('response', modelResponse || '')
// Create a secure temporary file instead of using the temp directory directly
responseFile = tmp.fileSync({
// Create a temporary file for the response that persists for downstream steps.
// We use keep: true to prevent automatic cleanup - the file will be cleaned up
// by the runner when the job completes.
const responseFile = tmp.fileSync({
prefix: 'modelResponse-',
postfix: '.txt',
keep: true,
})
core.setOutput('response-file', responseFile.name)
@@ -120,16 +119,6 @@ export async function run(): Promise<void> {
}
// Force exit to prevent hanging on open connections
process.exit(1)
} finally {
// Explicit cleanup of temporary file if it was created
if (responseFile) {
try {
responseFile.removeCallback()
} catch (cleanupError) {
// Log cleanup errors but don't fail the action
core.warning(`Failed to cleanup temporary file: ${cleanupError}`)
}
}
}
// Force exit to prevent hanging on open connections
+15 -5
View File
@@ -35,17 +35,27 @@ export interface GitHubMCPClient {
/**
* Connect to the GitHub MCP server and retrieve available tools
*/
export async function connectToGitHubMCP(token: string): Promise<GitHubMCPClient | null> {
export async function connectToGitHubMCP(token: string, toolsets?: string): Promise<GitHubMCPClient | null> {
const githubMcpUrl = 'https://api.githubcopilot.com/mcp/'
core.info('Connecting to GitHub MCP server...')
const headers: Record<string, string> = {
Authorization: `Bearer ${token}`,
'X-MCP-Readonly': 'true',
}
// Add toolsets header if specified
if (toolsets && toolsets.trim() !== '') {
headers['X-MCP-Toolsets'] = toolsets
core.info(`Using GitHub MCP toolsets: ${toolsets}`)
} else {
core.info('Using default GitHub MCP toolsets')
}
const transport = new StreamableHTTPClientTransport(new URL(githubMcpUrl), {
requestInit: {
headers: {
Authorization: `Bearer ${token}`,
'X-MCP-Readonly': 'true',
},
headers,
},
})