Sketch out MCP

This commit is contained in:
Sean Goedecke
2025-07-15 23:23:39 +00:00
parent 75eeed10d7
commit 0b82ac474e
8 changed files with 15531 additions and 220 deletions

View File

@@ -80,21 +80,52 @@ steps:
cat "${{ steps.inference.outputs.response-file }}"
```
### GitHub MCP Integration (Model Context Protocol)
This action now supports 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.
```yaml
steps:
- name: AI Inference with GitHub Tools
id: inference
uses: actions/ai-inference@v1
with:
prompt: 'List my open pull requests and create a summary'
enable-mcp: true
mcp-server-url: 'https://github-mcp-server.fly.dev/mcp' # Optional, this is the default
```
When MCP is enabled, the AI model will have access to GitHub tools and can
perform actions like:
- Listing and managing repositories
- Creating, reading, and updating issues
- Managing pull requests
- Searching code and repositories
- And more GitHub operations
**Note:** MCP integration requires appropriate GitHub permissions for the
operations the AI will perform.
## Inputs
Various inputs are defined in [`action.yml`](action.yml) to let you configure
the action:
| Name | Description | Default |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `token` | Token to use for inference. Typically the GITHUB_TOKEN secret | `github.token` |
| `prompt` | The prompt to send to the model | N/A |
| `prompt-file` | Path to a file containing the prompt. If both `prompt` and `prompt-file` are provided, `prompt-file` takes precedence | `""` |
| `system-prompt` | The system prompt to send to the model | `"You are a helpful assistant"` |
| `system-prompt-file` | Path to a file containing the system prompt. If both `system-prompt` and `system-prompt-file` are provided, `system-prompt-file` takes precedence | `""` |
| `model` | The model to use for inference. Must be available in the [GitHub Models](https://github.com/marketplace?type=models) catalog | `gpt-4o` |
| `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 |
| Name | Description | Default |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| `token` | Token to use for inference. Typically the GITHUB_TOKEN secret | `github.token` |
| `prompt` | The prompt to send to the model | N/A |
| `prompt-file` | Path to a file containing the prompt. If both `prompt` and `prompt-file` are provided, `prompt-file` takes precedence | `""` |
| `system-prompt` | The system prompt to send to the model | `"You are a helpful assistant"` |
| `system-prompt-file` | Path to a file containing the system prompt. If both `system-prompt` and `system-prompt-file` are provided, `system-prompt-file` takes precedence | `""` |
| `model` | The model to use for inference. Must be available in the [GitHub Models](https://github.com/marketplace?type=models) catalog | `gpt-4o` |
| `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-mcp` | Enable Model Context Protocol integration with GitHub tools | `false` |
| `mcp-server-url` | URL of the MCP server to connect to for GitHub tools | `https://github-mcp-server.fly.dev/mcp` |
## Outputs

View File

@@ -41,6 +41,14 @@ inputs:
description: The token to use
required: false
default: ${{ github.token }}
enable-mcp:
description: Enable Model Context Protocol integration with GitHub tools
required: false
default: 'false'
mcp-server-url:
description: URL of the MCP server to connect to
required: false
default: 'https://github-mcp-server.fly.dev/mcp'
# Define your outputs here.
outputs:

14872
dist/index.js generated vendored

File diff suppressed because it is too large Load Diff

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

694
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -37,12 +37,15 @@
},
"license": "MIT",
"dependencies": {
"@actions/core": "^1.11.1"
"@actions/core": "^1.11.1",
"@modelcontextprotocol/sdk": "^1.15.1",
"@rollup/plugin-json": "^6.1.0",
"pkce-challenge": "^5.0.0"
},
"devDependencies": {
"@azure-rest/ai-inference": "latest",
"@azure/core-auth": "latest",
"@azure/core-sse": "latest",
"@azure-rest/ai-inference": "^1.0.0-beta.6",
"@azure/core-auth": "^1.10.0",
"@azure/core-sse": "^2.3.0",
"@eslint/compat": "^1.2.9",
"@github/local-action": "^3.2.1",
"@jest/globals": "^29.7.0",

View File

@@ -3,6 +3,7 @@
import commonjs from '@rollup/plugin-commonjs'
import nodeResolve from '@rollup/plugin-node-resolve'
import typescript from '@rollup/plugin-typescript'
import json from '@rollup/plugin-json'
const config = {
input: 'src/index.ts',
@@ -12,7 +13,12 @@ const config = {
format: 'es',
sourcemap: true
},
plugins: [typescript(), nodeResolve({ preferBuiltins: true }), commonjs()]
plugins: [
typescript(),
nodeResolve({ preferBuiltins: true }),
commonjs(),
json()
]
}
export default config

View File

@@ -1,6 +1,8 @@
import * as core from '@actions/core'
import ModelClient, { isUnexpected } from '@azure-rest/ai-inference'
import { AzureKeyCredential } from '@azure/core-auth'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import * as fs from 'fs'
import * as os from 'os'
import * as path from 'path'
@@ -56,35 +58,93 @@ export async function run(): Promise<void> {
const modelName: string = core.getInput('model')
const maxTokens: number = parseInt(core.getInput('max-tokens'), 10)
const token = core.getInput('token') || process.env['GITHUB_TOKEN']
const token = process.env['GITHUB_TOKEN'] || core.getInput('token')
if (token === undefined) {
throw new Error('GITHUB_TOKEN is not set')
}
const endpoint = core.getInput('endpoint')
// Get MCP server configuration
const mcpServerUrl = 'https://api.githubcopilot.com/mcp/'
const enableMcp = core.getBooleanInput('enable-mcp') || false
let azureTools: any[] = []
// Connect to MCP server if enabled
if (enableMcp || true) {
core.info('Connecting to GitHub MCP server...' + token)
const transport = new StreamableHTTPClientTransport(
new URL(mcpServerUrl),
{
requestInit: {
headers: {
Authorization: `Bearer ${token}`
}
}
}
)
const mcp = new Client({
name: 'ai-inference-action',
version: '1.0.0',
transport
})
try {
await mcp.connect(transport)
} catch (mcpError) {
core.warning(`Failed to connect to MCP server: ${mcpError}`)
// Continue without tools if MCP connection fails
return
}
core.info('Successfully connected to MCP server')
// Pull tool metadata
const tools = await mcp.listTools()
core.info(`Retrieved ${tools.tools?.length || 0} tools from MCP server`)
// Map MCP → Azure tool definitions
azureTools = (tools.tools || []).map((t) => ({
type: 'function',
function: {
name: t.name,
description: t.description,
parameters: t.inputSchema
}
}))
core.info(`Mapped ${azureTools.length} tools for Azure AI Inference`)
}
const client = ModelClient(endpoint, new AzureKeyCredential(token), {
userAgentOptions: { userAgentPrefix: 'github-actions-ai-inference' }
})
const requestBody: any = {
messages: [
{
role: 'system',
content: systemPrompt
},
{ role: 'user', content: prompt }
],
max_tokens: maxTokens,
model: modelName
}
// Add tools if available
if (azureTools.length > 0) {
requestBody.tools = azureTools
}
const response = await client.path('/chat/completions').post({
body: {
messages: [
{
role: 'system',
content: systemPrompt
},
{ role: 'user', content: prompt }
],
max_tokens: maxTokens,
model: modelName
}
body: requestBody
})
if (isUnexpected(response)) {
if (response.body.error) {
throw response.body.error
}
throw new Error(
'An error occurred while fetching the response (' +
response.status +
@@ -96,6 +156,21 @@ export async function run(): Promise<void> {
const modelResponse: string | null =
response.body.choices[0].message.content
core.info(`Model response: ${response || 'No response content'}`)
// Handle tool calls if present
const toolCalls = response.body.choices[0].message.tool_calls
if (toolCalls && toolCalls.length > 0) {
core.info(`Model requested ${toolCalls.length} tool calls`)
// Note: For now, we'll just log the tool calls
// In a full implementation, you'd execute them via MCP and continue the conversation
for (const toolCall of toolCalls) {
core.info(
`Tool call: ${toolCall.function.name} with args: ${toolCall.function.arguments}`
)
}
}
// Set outputs for other workflow steps to use
core.setOutput('response', modelResponse || '')