Add read-only MCP support

This commit is contained in:
Sean Goedecke
2025-07-16 02:19:49 +00:00
parent 86c0691fbf
commit 4fd6464105
13 changed files with 182 additions and 802 deletions
+24 -11
View File
@@ -1,7 +1,7 @@
import * as core from '@actions/core'
import ModelClient, { isUnexpected } from '@azure-rest/ai-inference'
import { AzureKeyCredential } from '@azure/core-auth'
import { MCPClient, executeToolCalls } from './mcp.js'
import { GitHubMCPClient, executeToolCalls } from './mcp.js'
export interface InferenceRequest {
systemPrompt: string
@@ -14,7 +14,14 @@ export interface InferenceRequest {
export interface InferenceResponse {
content: string | null
toolCalls?: any[]
toolCalls?: Array<{
id: string
type: string
function: {
name: string
arguments: string
}
}>
}
/**
@@ -65,13 +72,13 @@ export async function simpleInference(
}
/**
* MCP-enabled inference with tool execution loop
* GitHub MCP-enabled inference with tool execution loop
*/
export async function mcpInference(
request: InferenceRequest,
mcpClient: MCPClient
githubMcpClient: GitHubMCPClient
): Promise<string | null> {
core.info('Running MCP inference with tools')
core.info('Running GitHub MCP inference with tools')
const client = ModelClient(
request.endpoint,
@@ -82,7 +89,7 @@ export async function mcpInference(
)
// Start with the initial conversation
let messages: any[] = [
const messages = [
{
role: 'system',
content: request.systemPrompt
@@ -101,7 +108,7 @@ export async function mcpInference(
messages: messages,
max_tokens: request.maxTokens,
model: request.modelName,
tools: mcpClient.tools
tools: githubMcpClient.tools
}
const response = await client.path('/chat/completions').post({
@@ -125,25 +132,31 @@ export async function mcpInference(
messages.push({
role: 'assistant',
content: modelResponse,
content: modelResponse || '',
...(toolCalls && { tool_calls: toolCalls })
})
if (!toolCalls || toolCalls.length === 0) {
core.info('No tool calls requested, ending MCP inference loop')
core.info('No tool calls requested, ending GitHub MCP inference loop')
return modelResponse
}
core.info(`Model requested ${toolCalls.length} tool calls`)
const toolResults = await executeToolCalls(mcpClient.client, toolCalls)
// Execute all tool calls via GitHub MCP
const toolResults = await executeToolCalls(
githubMcpClient.client,
toolCalls
)
// Add tool results to the conversation
messages.push(...toolResults)
core.info('Tool results added, continuing conversation...')
}
core.warning(
`MCP inference loop exceeded maximum iterations (${maxIterations})`
`GitHub MCP inference loop exceeded maximum iterations (${maxIterations})`
)
// Return the last assistant message content
+3 -3
View File
@@ -2,7 +2,7 @@ import * as core from '@actions/core'
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
import { connectToMCP } from './mcp.js'
import { connectToGitHubMCP } from './mcp.js'
import { simpleInference, mcpInference, InferenceRequest } from './inference.js'
import { loadContentFromFileOrInput } from './helpers.js'
@@ -32,7 +32,7 @@ export async function run(): Promise<void> {
}
const endpoint = core.getInput('endpoint')
const enableMcp = core.getBooleanInput('enable-mcp') || false
const enableMcp = core.getBooleanInput('enable-github-mcp') || false
const inferenceRequest: InferenceRequest = {
systemPrompt,
@@ -46,7 +46,7 @@ export async function run(): Promise<void> {
let modelResponse: string | null = null
if (enableMcp) {
const mcpClient = await connectToMCP(token)
const mcpClient = await connectToGitHubMCP(token)
if (mcpClient) {
modelResponse = await mcpInference(inferenceRequest, mcpClient)
+45 -29
View File
@@ -9,23 +9,44 @@ export interface ToolResult {
content: string
}
export interface MCPClient {
export interface MCPTool {
type: 'function'
function: {
name: string
description?: string
parameters?: Record<string, unknown>
}
}
export interface ToolCall {
id: string
type: string
function: {
name: string
arguments: string
}
}
export interface GitHubMCPClient {
client: Client
tools: any[]
tools: Array<MCPTool>
}
/**
* Connect to the MCP server and retrieve available tools
* Connect to the GitHub MCP server and retrieve available tools
*/
export async function connectToMCP(token: string): Promise<MCPClient | null> {
const mcpServerUrl = 'https://api.githubcopilot.com/mcp/'
export async function connectToGitHubMCP(
token: string
): Promise<GitHubMCPClient | null> {
const githubMcpUrl = 'https://api.githubcopilot.com/mcp/'
core.info('Connecting to GitHub MCP server...')
const transport = new StreamableHTTPClientTransport(new URL(mcpServerUrl), {
const transport = new StreamableHTTPClientTransport(new URL(githubMcpUrl), {
requestInit: {
headers: {
Authorization: `Bearer ${token}`
Authorization: `Bearer ${token}`,
'X-MCP-Readonly': 'true'
}
}
})
@@ -39,21 +60,20 @@ export async function connectToMCP(token: string): Promise<MCPClient | null> {
try {
await client.connect(transport)
} catch (mcpError) {
core.warning(`Failed to connect to MCP server: ${mcpError}`)
core.warning(`Failed to connect to GitHub MCP server: ${mcpError}`)
return null
}
core.info('Successfully connected to MCP server')
core.info('Successfully connected to GitHub MCP server')
// Pull tool metadata
const toolsResponse = await client.listTools()
core.info(
`Retrieved ${toolsResponse.tools?.length || 0} tools from MCP server`
`Retrieved ${toolsResponse.tools?.length || 0} tools from GitHub MCP server`
)
// Map MCP → Azure tool definitions
// Map GitHub MCP tools → Azure AI Inference tool definitions
const tools = (toolsResponse.tools || []).map((t) => ({
type: 'function',
type: 'function' as const,
function: {
name: t.name,
description: t.description,
@@ -61,35 +81,32 @@ export async function connectToMCP(token: string): Promise<MCPClient | null> {
}
}))
core.info(`Mapped ${tools.length} tools for Azure AI Inference`)
core.info(`Mapped ${tools.length} GitHub MCP tools for Azure AI Inference`)
return { client, tools }
}
/**
* Execute a single tool call via MCP
* Execute a single tool call via GitHub MCP
*/
export async function executeToolCall(
mcpClient: Client,
toolCall: any
githubMcpClient: Client,
toolCall: ToolCall
): Promise<ToolResult> {
core.info(
`Executing tool: ${toolCall.function.name} with args: ${toolCall.function.arguments}`
`Executing GitHub MCP tool: ${toolCall.function.name} with args: ${toolCall.function.arguments}`
)
try {
// Parse the arguments from JSON string
const args = JSON.parse(toolCall.function.arguments)
// Call the tool via MCP
const result = await mcpClient.callTool({
const result = await githubMcpClient.callTool({
name: toolCall.function.name,
arguments: args
})
core.info(`Tool ${toolCall.function.name} executed successfully`)
core.info(`GitHub MCP tool ${toolCall.function.name} executed successfully`)
// Return the result formatted for the conversation
return {
tool_call_id: toolCall.id,
role: 'tool',
@@ -98,10 +115,9 @@ export async function executeToolCall(
}
} catch (toolError) {
core.warning(
`Failed to execute tool ${toolCall.function.name}: ${toolError}`
`Failed to execute GitHub MCP tool ${toolCall.function.name}: ${toolError}`
)
// Return error result to continue conversation
return {
tool_call_id: toolCall.id,
role: 'tool',
@@ -112,16 +128,16 @@ export async function executeToolCall(
}
/**
* Execute all tool calls from a response
* Execute all tool calls from a response via GitHub MCP
*/
export async function executeToolCalls(
mcpClient: Client,
toolCalls: any[]
githubMcpClient: Client,
toolCalls: ToolCall[]
): Promise<ToolResult[]> {
const toolResults: ToolResult[] = []
for (const toolCall of toolCalls) {
const result = await executeToolCall(mcpClient, toolCall)
const result = await executeToolCall(githubMcpClient, toolCall)
toolResults.push(result)
}