Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
334892bb20 | ||
|
|
bbe0ccb244 | ||
|
|
ca3b99ea74 | ||
|
|
8a5d2ea4a1 | ||
|
|
112739fb15 | ||
|
|
f95554969e | ||
|
|
9e60aa0a3f | ||
|
|
02c6cc30ae | ||
|
|
18d468666d | ||
|
|
fd73d0264c | ||
|
|
27350b2a98 | ||
|
|
e8987e92e0 | ||
|
|
2d03946378 | ||
|
|
d061fc5469 | ||
|
|
2d2f67ec42 | ||
|
|
9170087739 | ||
|
|
62db90ab13 | ||
|
|
16f2d5c46b | ||
|
|
95443f8d18 | ||
|
|
5022b33bc1 | ||
|
|
c9e14713bc | ||
|
|
39308142df | ||
|
|
48f0edec4d | ||
|
|
36ea1371dc | ||
|
|
de16a30c20 | ||
|
|
48758ceaff | ||
|
|
dd3dff10ba | ||
|
|
4bb01ee5ee | ||
|
|
4b4b2e8afe | ||
|
|
932a853db4 | ||
|
|
e0da58c63f | ||
|
|
af1c1c29a3 | ||
|
|
83bb5ca3e8 | ||
|
|
4d2337d006 | ||
|
|
7ba7530ad4 | ||
|
|
4d7d83c494 | ||
|
|
a1c1182922 | ||
|
|
dfaa426c29 | ||
|
|
7fa0024f13 | ||
|
|
fc6f9a0800 | ||
|
|
a1d07305b7 | ||
|
|
6e0d8949d8 | ||
|
|
f347eae8eb | ||
|
|
07fe2f30ad | ||
|
|
1843310df4 | ||
|
|
c72cb2ef9c | ||
|
|
a2fd223fcf | ||
|
|
3ba8e1b39d | ||
|
|
52e5222a82 | ||
|
|
a62dfeda7b | ||
|
|
48235f7026 | ||
|
|
9bbcef8fa4 |
6
.github/workflows/check-dist.yml
vendored
6
.github/workflows/check-dist.yml
vendored
@@ -28,11 +28,11 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
id: checkout
|
||||
uses: actions/checkout@v4
|
||||
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/
|
||||
|
||||
83
.github/workflows/ci.yml
vendored
83
.github/workflows/ci.yml
vendored
@@ -20,11 +20,11 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
id: checkout
|
||||
uses: actions/checkout@v4
|
||||
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@v4
|
||||
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@v4
|
||||
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
|
||||
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -30,19 +30,19 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
id: checkout
|
||||
uses: actions/checkout@v4
|
||||
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
|
||||
|
||||
8
.github/workflows/licensed.yml
vendored
8
.github/workflows/licensed.yml
vendored
@@ -27,11 +27,11 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
id: checkout
|
||||
uses: actions/checkout@v4
|
||||
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,11 +42,11 @@ jobs:
|
||||
|
||||
- name: Setup Ruby
|
||||
id: setup-ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71
|
||||
with:
|
||||
ruby-version: ruby
|
||||
|
||||
- uses: licensee/setup-licensed@v1.3.2
|
||||
- uses: licensee/setup-licensed@0d52e575b3258417672be0dff2f115d7db8771d8
|
||||
with:
|
||||
version: 4.x
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
2
.github/workflows/linter.yml
vendored
2
.github/workflows/linter.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
id: checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
32
.licenses/npm/@types/tmp.dep.yml
Normal file
32
.licenses/npm/@types/tmp.dep.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: "@types/tmp"
|
||||
version: 0.2.6
|
||||
type: npm
|
||||
summary: TypeScript definitions for tmp
|
||||
homepage: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/tmp
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |2
|
||||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE
|
||||
notices: []
|
||||
32
.licenses/npm/tmp.dep.yml
Normal file
32
.licenses/npm/tmp.dep.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: tmp
|
||||
version: 0.2.5
|
||||
type: npm
|
||||
summary: Temporary file and directory creator
|
||||
homepage: http://github.com/raszi/node-tmp
|
||||
license: mit
|
||||
licenses:
|
||||
- sources: LICENSE
|
||||
text: |
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 KARASZI István
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
notices: []
|
||||
@@ -1 +1 @@
|
||||
20.9.0
|
||||
24.4.0
|
||||
38
README.md
38
README.md
@@ -162,6 +162,19 @@ 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.
|
||||
|
||||
#### 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:
|
||||
- name: AI Inference with GitHub Tools
|
||||
@@ -170,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
|
||||
@@ -185,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.
|
||||
|
||||
@@ -209,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). Use a separate PAT for tighter security | `""` |
|
||||
| `github-mcp-token` | Token to use for GitHub MCP server (defaults to the main token if not specified). | `""` |
|
||||
|
||||
## Outputs
|
||||
|
||||
|
||||
@@ -106,6 +106,8 @@ describe('helpers.ts - inference request building', () => {
|
||||
undefined,
|
||||
undefined,
|
||||
'gpt-4',
|
||||
undefined,
|
||||
undefined,
|
||||
100,
|
||||
'https://api.test.com',
|
||||
'test-token',
|
||||
@@ -117,6 +119,8 @@ describe('helpers.ts - inference request building', () => {
|
||||
{role: 'user', content: 'User message'},
|
||||
],
|
||||
modelName: 'gpt-4',
|
||||
temperature: undefined,
|
||||
topP: undefined,
|
||||
maxTokens: 100,
|
||||
endpoint: 'https://api.test.com',
|
||||
token: 'test-token',
|
||||
@@ -136,6 +140,8 @@ describe('helpers.ts - inference request building', () => {
|
||||
'System prompt',
|
||||
'User prompt',
|
||||
'gpt-4',
|
||||
undefined,
|
||||
undefined,
|
||||
100,
|
||||
'https://api.test.com',
|
||||
'test-token',
|
||||
@@ -147,6 +153,8 @@ describe('helpers.ts - inference request building', () => {
|
||||
{role: 'user', content: 'User prompt'},
|
||||
],
|
||||
modelName: 'gpt-4',
|
||||
temperature: undefined,
|
||||
topP: undefined,
|
||||
maxTokens: 100,
|
||||
endpoint: 'https://api.test.com',
|
||||
token: 'test-token',
|
||||
|
||||
@@ -66,7 +66,7 @@ function mockInputs(inputs: Record<string, string> = {}): void {
|
||||
*/
|
||||
function verifyStandardResponse(): void {
|
||||
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'response', 'Hello, user!')
|
||||
expect(core.setOutput).toHaveBeenNthCalledWith(2, 'response-file', expect.stringContaining('modelResponse.txt'))
|
||||
expect(core.setOutput).toHaveBeenNthCalledWith(2, 'response-file', expect.stringContaining('modelResponse-'))
|
||||
}
|
||||
|
||||
vi.mock('fs', () => ({
|
||||
@@ -75,6 +75,15 @@ vi.mock('fs', () => ({
|
||||
writeFileSync: mockWriteFileSync,
|
||||
}))
|
||||
|
||||
// Mocks for tmp module to control temporary file creation
|
||||
const mockFileSync = vi.fn().mockReturnValue({
|
||||
name: '/secure/temp/dir/modelResponse-abc123.txt',
|
||||
})
|
||||
|
||||
vi.mock('tmp', () => ({
|
||||
fileSync: mockFileSync,
|
||||
}))
|
||||
|
||||
// Mock MCP and inference modules
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const mockConnectToGitHubMCP = vi.fn() as MockedFunction<any>
|
||||
@@ -186,7 +195,7 @@ describe('main.ts', () => {
|
||||
|
||||
await run()
|
||||
|
||||
expect(mockConnectToGitHubMCP).toHaveBeenCalledWith('fake-token')
|
||||
expect(mockConnectToGitHubMCP).toHaveBeenCalledWith('fake-token', '')
|
||||
expect(mockMcpInference).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
messages: [
|
||||
@@ -213,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')
|
||||
@@ -269,4 +278,25 @@ describe('main.ts', () => {
|
||||
expect(core.setFailed).toHaveBeenCalledWith(`File for prompt-file was not found: ${promptFile}`)
|
||||
expect(mockProcessExit).toHaveBeenCalledWith(1)
|
||||
})
|
||||
|
||||
it('creates temporary files that persist for downstream steps', async () => {
|
||||
mockInputs({
|
||||
prompt: 'Test prompt',
|
||||
'system-prompt': 'You are a test assistant.',
|
||||
})
|
||||
|
||||
await run()
|
||||
|
||||
// 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(mockProcessExit).toHaveBeenCalledWith(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -55,7 +55,11 @@ inputs:
|
||||
required: false
|
||||
default: 'false'
|
||||
github-mcp-token:
|
||||
description: The token to use for GitHub MCP server (defaults to GITHUB_TOKEN if not specified)
|
||||
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: ''
|
||||
|
||||
@@ -67,5 +71,5 @@ outputs:
|
||||
description: The file path where the response is saved
|
||||
|
||||
runs:
|
||||
using: node20
|
||||
using: node24
|
||||
main: dist/index.js
|
||||
|
||||
1041
dist/index.js
generated
vendored
1041
dist/index.js
generated
vendored
File diff suppressed because it is too large
Load Diff
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -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',
|
||||
|
||||
37
package-lock.json
generated
37
package-lock.json
generated
@@ -11,9 +11,11 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.11.1",
|
||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||
"@types/tmp": "^0.2.6",
|
||||
"js-yaml": "^4.1.0",
|
||||
"openai": "^5.11.0",
|
||||
"pkce-challenge": "^5.0.0"
|
||||
"pkce-challenge": "^5.0.0",
|
||||
"tmp": "^0.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.3.0",
|
||||
@@ -24,7 +26,7 @@
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/node": "^24.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.34.0",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"eslint": "^9.29.0",
|
||||
@@ -39,7 +41,7 @@
|
||||
"vitest": "^3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
"node": ">=24"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-x64-gnu": "*"
|
||||
@@ -2476,13 +2478,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.15.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.31.tgz",
|
||||
"integrity": "sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==",
|
||||
"version": "24.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
|
||||
"integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
"undici-types": "~7.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
@@ -2492,6 +2494,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/tmp": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz",
|
||||
"integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.34.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.0.tgz",
|
||||
@@ -8943,6 +8951,15 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
||||
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -9207,9 +9224,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
".": "./dist/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
"node": ">=24"
|
||||
},
|
||||
"scripts": {
|
||||
"bundle": "npm run format:write && npm run package",
|
||||
@@ -25,9 +25,11 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.11.1",
|
||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||
"@types/tmp": "^0.2.6",
|
||||
"js-yaml": "^4.1.0",
|
||||
"openai": "^5.11.0",
|
||||
"pkce-challenge": "^5.0.0"
|
||||
"pkce-challenge": "^5.0.0",
|
||||
"tmp": "^0.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.3.0",
|
||||
@@ -38,7 +40,7 @@
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^22.15.31",
|
||||
"@types/node": "^24.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.34.0",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"eslint": "^9.29.0",
|
||||
|
||||
71
script/mock-inference-server.mjs
Normal file
71
script/mock-inference-server.mjs
Normal 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')
|
||||
})
|
||||
@@ -82,6 +82,8 @@ export function buildInferenceRequest(
|
||||
systemPrompt: string | undefined,
|
||||
prompt: string | undefined,
|
||||
modelName: string,
|
||||
temperature: number | undefined,
|
||||
topP: number | undefined,
|
||||
maxTokens: number,
|
||||
endpoint: string,
|
||||
token: string,
|
||||
@@ -92,6 +94,8 @@ export function buildInferenceRequest(
|
||||
return {
|
||||
messages,
|
||||
modelName,
|
||||
temperature,
|
||||
topP,
|
||||
maxTokens,
|
||||
endpoint,
|
||||
token,
|
||||
|
||||
@@ -15,6 +15,8 @@ export interface InferenceRequest {
|
||||
maxTokens: number
|
||||
endpoint: string
|
||||
token: string
|
||||
temperature?: number
|
||||
topP?: number
|
||||
responseFormat?: {type: 'json_schema'; json_schema: unknown} // Processed response format for the API
|
||||
}
|
||||
|
||||
@@ -45,6 +47,8 @@ export async function simpleInference(request: InferenceRequest): Promise<string
|
||||
messages: request.messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[],
|
||||
max_tokens: request.maxTokens,
|
||||
model: request.modelName,
|
||||
temperature: request.temperature,
|
||||
top_p: request.topP,
|
||||
}
|
||||
|
||||
// Add response format if specified
|
||||
@@ -53,21 +57,10 @@ export async function simpleInference(request: InferenceRequest): Promise<string
|
||||
chatCompletionRequest.response_format = request.responseFormat as any
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await client.chat.completions.create(chatCompletionRequest)
|
||||
|
||||
if ('choices' in response) {
|
||||
const modelResponse = response.choices[0]?.message?.content
|
||||
core.info(`Model response: ${modelResponse || 'No response content'}`)
|
||||
return modelResponse || null
|
||||
} else {
|
||||
core.error(`Unexpected response format from API: ${JSON.stringify(response)}`)
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
core.error(`API error: ${error}`)
|
||||
throw error
|
||||
}
|
||||
const response = await chatCompletion(client, chatCompletionRequest, 'simpleInference')
|
||||
const modelResponse = response.choices[0]?.message?.content
|
||||
core.info(`Model response: ${modelResponse || 'No response content'}`)
|
||||
return modelResponse || null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,6 +94,8 @@ export async function mcpInference(
|
||||
messages: messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[],
|
||||
max_tokens: request.maxTokens,
|
||||
model: request.modelName,
|
||||
temperature: request.temperature,
|
||||
top_p: request.topP,
|
||||
}
|
||||
|
||||
// Add response format if specified (only on final iteration to avoid conflicts with tool calls)
|
||||
@@ -112,11 +107,7 @@ export async function mcpInference(
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await client.chat.completions.create(chatCompletionRequest)
|
||||
|
||||
if (!('choices' in response)) {
|
||||
throw new Error(`Unexpected response format from API: ${JSON.stringify(response)}`)
|
||||
}
|
||||
const response = await chatCompletion(client, chatCompletionRequest, `mcpInference iteration ${iterationCount}`)
|
||||
|
||||
const assistantMessage = response.choices[0]?.message
|
||||
const modelResponse = assistantMessage?.content
|
||||
@@ -133,20 +124,13 @@ export async function mcpInference(
|
||||
if (!toolCalls || toolCalls.length === 0) {
|
||||
core.info('No tool calls requested, ending GitHub MCP inference loop')
|
||||
|
||||
// If we have a response format set and we haven't explicitly run one final message iteration,
|
||||
// do another loop with the response format set
|
||||
if (request.responseFormat && !finalMessage) {
|
||||
core.info('Making one more MCP loop with the requested response format...')
|
||||
|
||||
// Add a user message requesting JSON format and try again
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: `Please provide your response in the exact ${request.responseFormat.type} format specified.`,
|
||||
})
|
||||
|
||||
finalMessage = true
|
||||
|
||||
// Continue the loop to get a properly formatted response
|
||||
continue
|
||||
} else {
|
||||
return modelResponse || null
|
||||
@@ -154,13 +138,8 @@ export async function mcpInference(
|
||||
}
|
||||
|
||||
core.info(`Model requested ${toolCalls.length} tool calls`)
|
||||
|
||||
// Execute all tool calls via GitHub MCP
|
||||
const toolResults = await executeToolCalls(githubMcpClient.client, toolCalls as ToolCall[])
|
||||
|
||||
// Add tool results to the conversation
|
||||
messages.push(...toolResults)
|
||||
|
||||
core.info('Tool results added, continuing conversation...')
|
||||
} catch (error) {
|
||||
core.error(`OpenAI API error: ${error}`)
|
||||
@@ -178,3 +157,43 @@ export async function mcpInference(
|
||||
|
||||
return lastAssistantMessage?.content || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around OpenAI chat.completions.create with defensive handling for cases where
|
||||
* the SDK returns a raw string (e.g., unexpected content-type or streaming body) instead of
|
||||
* a parsed object. Ensures an object with a 'choices' array is returned or throws a descriptive error.
|
||||
*/
|
||||
async function chatCompletion(
|
||||
client: OpenAI,
|
||||
params: OpenAI.Chat.Completions.ChatCompletionCreateParams,
|
||||
context: string,
|
||||
): Promise<OpenAI.Chat.Completions.ChatCompletion> {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let response: any = await client.chat.completions.create(params)
|
||||
core.debug(`${context}: raw response typeof=${typeof response}`)
|
||||
|
||||
if (typeof response === 'string') {
|
||||
// Attempt to parse if we unexpectedly received a string
|
||||
try {
|
||||
response = JSON.parse(response)
|
||||
} catch (e) {
|
||||
const preview = response.slice(0, 400)
|
||||
throw new Error(
|
||||
`${context}: Chat completion response was a string and not valid JSON (${(e as Error).message}). Preview: ${preview}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!response || typeof response !== 'object' || !('choices' in response)) {
|
||||
const preview = JSON.stringify(response)?.slice(0, 800)
|
||||
throw new Error(`${context}: Unexpected response shape (no choices). Preview: ${preview}`)
|
||||
}
|
||||
|
||||
return response as OpenAI.Chat.Completions.ChatCompletion
|
||||
} catch (err) {
|
||||
// Re-throw after logging for upstream handling
|
||||
core.error(`${context}: chatCompletion failed: ${err}`)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
35
src/main.ts
35
src/main.ts
@@ -1,7 +1,6 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
import * as tmp from 'tmp'
|
||||
import {connectToGitHubMCP} from './mcp.js'
|
||||
import {simpleInference, mcpInference} from './inference.js'
|
||||
import {loadContentFromFileOrInput, buildInferenceRequest} from './helpers.js'
|
||||
@@ -13,8 +12,6 @@ import {
|
||||
parseFileTemplateVariables,
|
||||
} from './prompt.js'
|
||||
|
||||
const RESPONSE_FILE = 'modelResponse.txt'
|
||||
|
||||
/**
|
||||
* The main function for the action.
|
||||
*
|
||||
@@ -51,7 +48,11 @@ export async function run(): Promise<void> {
|
||||
|
||||
// Get common parameters
|
||||
const modelName = promptConfig?.model || core.getInput('model')
|
||||
const maxTokens = parseInt(core.getInput('max-tokens'), 10)
|
||||
let maxTokens = promptConfig?.modelParameters?.maxTokens ?? core.getInput('max-tokens')
|
||||
|
||||
if (typeof maxTokens === 'string') {
|
||||
maxTokens = parseInt(maxTokens, 10)
|
||||
}
|
||||
|
||||
const token = process.env['GITHUB_TOKEN'] || core.getInput('token')
|
||||
if (token === undefined) {
|
||||
@@ -60,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')
|
||||
|
||||
@@ -69,6 +71,8 @@ export async function run(): Promise<void> {
|
||||
systemPrompt,
|
||||
prompt,
|
||||
modelName,
|
||||
promptConfig?.modelParameters?.temperature,
|
||||
promptConfig?.modelParameters?.topP,
|
||||
maxTokens,
|
||||
endpoint,
|
||||
token,
|
||||
@@ -79,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)
|
||||
@@ -93,11 +97,19 @@ export async function run(): Promise<void> {
|
||||
|
||||
core.setOutput('response', modelResponse || '')
|
||||
|
||||
const responseFilePath = path.join(tempDir(), RESPONSE_FILE)
|
||||
core.setOutput('response-file', responseFilePath)
|
||||
// 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)
|
||||
|
||||
if (modelResponse && modelResponse !== '') {
|
||||
fs.writeFileSync(responseFilePath, modelResponse, 'utf-8')
|
||||
fs.writeFileSync(responseFile.name, modelResponse, 'utf-8')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
@@ -112,8 +124,3 @@ export async function run(): Promise<void> {
|
||||
// Force exit to prevent hanging on open connections
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
function tempDir(): string {
|
||||
const tempDirectory = process.env['RUNNER_TEMP'] || os.tmpdir()
|
||||
return tempDirectory
|
||||
}
|
||||
|
||||
20
src/mcp.ts
20
src/mcp.ts
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -7,9 +7,16 @@ export interface PromptMessage {
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface ModelParameters {
|
||||
maxTokens?: number
|
||||
temperature?: number
|
||||
topP?: number
|
||||
}
|
||||
|
||||
export interface PromptConfig {
|
||||
messages: PromptMessage[]
|
||||
model?: string
|
||||
modelParameters?: ModelParameters
|
||||
responseFormat?: 'text' | 'json_schema'
|
||||
jsonSchema?: string
|
||||
}
|
||||
@@ -101,11 +108,8 @@ export function loadPromptFile(filePath: string, templateVariables: TemplateVari
|
||||
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
||||
|
||||
// Apply template variable substitution
|
||||
const processedContent = replaceTemplateVariables(fileContent, templateVariables)
|
||||
|
||||
try {
|
||||
const config = yaml.load(processedContent) as PromptConfig
|
||||
const config = yaml.load(fileContent) as PromptConfig
|
||||
|
||||
if (!config.messages || !Array.isArray(config.messages)) {
|
||||
throw new Error('Prompt file must contain a "messages" array')
|
||||
@@ -121,6 +125,14 @@ export function loadPromptFile(filePath: string, templateVariables: TemplateVari
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare messages by replacing template variables with actual content
|
||||
config.messages = config.messages.map(msg => {
|
||||
return {
|
||||
...msg,
|
||||
content: replaceTemplateVariables(msg.content, templateVariables),
|
||||
}
|
||||
})
|
||||
|
||||
return config
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse prompt file: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
|
||||
Reference in New Issue
Block a user