Skip to content

Commit 0f8d1fa

Browse files
authored
Merge branch 'main' into feature/http-listen-address
2 parents 4be8c73 + 3422703 commit 0f8d1fa

105 files changed

Lines changed: 8858 additions & 1229 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/docker-publish.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,13 @@ jobs:
5454
# multi-platform images and export cache
5555
# https://github.com/docker/setup-buildx-action
5656
- name: Set up Docker Buildx
57-
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
57+
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
5858

5959
# Login against a Docker registry except on PR
6060
# https://github.com/docker/login-action
6161
- name: Log into registry ${{ env.REGISTRY }}
6262
if: github.event_name != 'pull_request'
63-
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
63+
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
6464
with:
6565
registry: ${{ env.REGISTRY }}
6666
username: ${{ github.actor }}
@@ -70,7 +70,7 @@ jobs:
7070
# https://github.com/docker/metadata-action
7171
- name: Extract Docker metadata
7272
id: meta
73-
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
73+
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
7474
with:
7575
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
7676
tags: |
@@ -106,7 +106,7 @@ jobs:
106106
# https://github.com/docker/build-push-action
107107
- name: Build and push Docker image
108108
id: build-and-push
109-
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
109+
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
110110
with:
111111
context: .
112112
push: ${{ github.event_name != 'pull_request' }}

.github/workflows/mcp-diff.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
fetch-depth: 0
2121

2222
- name: Set up Go
23-
uses: actions/setup-go@v5
23+
uses: actions/setup-go@v6
2424
with:
2525
go-version-file: go.mod
2626

@@ -85,7 +85,7 @@ jobs:
8585
fetch-depth: 0
8686

8787
- name: Set up Go
88-
uses: actions/setup-go@v5
88+
uses: actions/setup-go@v6
8989
with:
9090
go-version-file: go.mod
9191

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM node:26-alpine@sha256:e71ac5e964b9201072425d59d2e876359efa25dc96bb1768cb73295728d6e4ea AS ui-build
1+
FROM node:26-alpine@sha256:144769ec3f32e8ee36b3cfde91e82bee25d9367b20f31a151f3f7eea3a2a8541 AS ui-build
22
WORKDIR /app
33
COPY ui/package*.json ./ui/
44
RUN cd ui && npm ci
@@ -7,7 +7,7 @@ COPY ui/ ./ui/
77
RUN mkdir -p ./pkg/github/ui_dist && \
88
cd ui && npm run build
99

10-
FROM golang:1.25.10-alpine@sha256:8d22e29d960bc50cd025d93d5b7c7d220b1ee9aa7a239b3c8f55a57e987e8d45 AS build
10+
FROM golang:1.25.11-alpine@sha256:cd2fb3559df6e13bc93b7f0734a4eabe1d21e7b64eec211ed90784f00a17a56a AS build
1111
ARG VERSION="dev"
1212

1313
# Set the working directory

README.md

Lines changed: 27 additions & 19 deletions
Large diffs are not rendered by default.

cmd/github-mcp-server/generate_docs.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -365,9 +365,11 @@ func generateRemoteToolsetsDoc() string {
365365
buf.WriteString("| Name | Description | API URL | 1-Click Install (VS Code) | Read-only Link | 1-Click Read-only Install (VS Code) |\n")
366366
buf.WriteString("| ---- | ----------- | ------- | ------------------------- | -------------- | ----------------------------------- |\n")
367367

368-
// Add "all" toolset first (special case)
369-
allIcon := octiconImg("apps", "../")
370-
fmt.Fprintf(&buf, "| %s<br>`all` | All available GitHub MCP tools | https://api.githubcopilot.com/mcp/ | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%%7B%%22type%%22%%3A%%20%%22http%%22%%2C%%22url%%22%%3A%%20%%22https%%3A%%2F%%2Fapi.githubcopilot.com%%2Fmcp%%2F%%22%%7D) | [read-only](https://api.githubcopilot.com/mcp/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%%7B%%22type%%22%%3A%%20%%22http%%22%%2C%%22url%%22%%3A%%20%%22https%%3A%%2F%%2Fapi.githubcopilot.com%%2Fmcp%%2Freadonly%%22%%7D) |\n", allIcon)
368+
// Add "default" and "all" meta toolsets first (special cases). The base
369+
// URL serves the default toolset; /x/all enables every toolset at once.
370+
metaIcon := octiconImg("apps", "../")
371+
fmt.Fprintf(&buf, "| %s<br>`default` | Default toolset | https://api.githubcopilot.com/mcp/ | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%%7B%%22type%%22%%3A%%20%%22http%%22%%2C%%22url%%22%%3A%%20%%22https%%3A%%2F%%2Fapi.githubcopilot.com%%2Fmcp%%2F%%22%%7D) | [read-only](https://api.githubcopilot.com/mcp/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=github&config=%%7B%%22type%%22%%3A%%20%%22http%%22%%2C%%22url%%22%%3A%%20%%22https%%3A%%2F%%2Fapi.githubcopilot.com%%2Fmcp%%2Freadonly%%22%%7D) |\n", metaIcon)
372+
fmt.Fprintf(&buf, "| %s<br>`all` | All available GitHub MCP tools | https://api.githubcopilot.com/mcp/x/all | [Install](https://insiders.vscode.dev/redirect/mcp/install?name=gh-all&config=%%7B%%22type%%22%%3A%%20%%22http%%22%%2C%%22url%%22%%3A%%20%%22https%%3A%%2F%%2Fapi.githubcopilot.com%%2Fmcp%%2Fx%%2Fall%%22%%7D) | [read-only](https://api.githubcopilot.com/mcp/x/all/readonly) | [Install read-only](https://insiders.vscode.dev/redirect/mcp/install?name=gh-all&config=%%7B%%22type%%22%%3A%%20%%22http%%22%%2C%%22url%%22%%3A%%20%%22https%%3A%%2F%%2Fapi.githubcopilot.com%%2Fmcp%%2Fx%%2Fall%%2Freadonly%%22%%7D) |\n", metaIcon)
371373

372374
// AvailableToolsets() returns toolsets that have tools, sorted by ID
373375
// Exclude context (handled separately)

cmd/mcpcurl/main.go

Lines changed: 104 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package main
22

33
import (
4-
"bytes"
4+
"bufio"
55
"crypto/rand"
66
"encoding/json"
77
"fmt"
@@ -376,8 +376,8 @@ func buildJSONRPCRequest(method, toolName string, arguments map[string]any) (str
376376
return string(jsonData), nil
377377
}
378378

379-
// executeServerCommand runs the specified command, sends the JSON request to stdin,
380-
// and returns the response from stdout
379+
// executeServerCommand runs the specified command, performs the MCP initialization
380+
// handshake, sends the JSON request to stdin, and returns the response from stdout.
381381
func executeServerCommand(cmdStr, jsonRequest string) (string, error) {
382382
// Split the command string into command and arguments
383383
cmdParts := strings.Fields(cmdStr)
@@ -393,28 +393,119 @@ func executeServerCommand(cmdStr, jsonRequest string) (string, error) {
393393
return "", fmt.Errorf("failed to create stdin pipe: %w", err)
394394
}
395395

396-
// Setup stdout and stderr pipes
397-
var stdout, stderr bytes.Buffer
398-
cmd.Stdout = &stdout
396+
// Setup stdout pipe for line-by-line reading
397+
stdoutPipe, err := cmd.StdoutPipe()
398+
if err != nil {
399+
return "", fmt.Errorf("failed to create stdout pipe: %w", err)
400+
}
401+
402+
// Stderr still uses a buffer
403+
var stderr strings.Builder
399404
cmd.Stderr = &stderr
400405

401406
// Start the command
402407
if err := cmd.Start(); err != nil {
403408
return "", fmt.Errorf("failed to start command: %w", err)
404409
}
405410

406-
// Write the JSON request to stdin
411+
// Ensure the child process is cleaned up on every return path.
412+
// stdin must be closed before Wait so the server sees EOF and exits;
413+
// its non-zero exit status on EOF is expected, so we ignore the error.
414+
defer func() {
415+
_ = stdin.Close()
416+
_ = cmd.Wait()
417+
}()
418+
419+
// Use a scanner with a large buffer for reading JSON-RPC responses
420+
scanner := bufio.NewScanner(stdoutPipe)
421+
scanner.Buffer(make([]byte, 0, 1024*1024), 1024*1024) // 1MB max line size
422+
423+
// Step 1: Send MCP initialize request
424+
initReq, err := buildInitializeRequest()
425+
if err != nil {
426+
return "", fmt.Errorf("failed to build initialize request: %w", err)
427+
}
428+
if _, err := io.WriteString(stdin, initReq+"\n"); err != nil {
429+
return "", fmt.Errorf("failed to write initialize request: %w", err)
430+
}
431+
432+
// Step 2: Read initialize response (skip any server notifications)
433+
if _, err := readJSONRPCResponse(scanner); err != nil {
434+
return "", fmt.Errorf("failed to read initialize response: %w, stderr: %s", err, stderr.String())
435+
}
436+
437+
// Step 3: Send initialized notification
438+
if _, err := io.WriteString(stdin, buildInitializedNotification()+"\n"); err != nil {
439+
return "", fmt.Errorf("failed to write initialized notification: %w", err)
440+
}
441+
442+
// Step 4: Send the actual request
407443
if _, err := io.WriteString(stdin, jsonRequest+"\n"); err != nil {
408-
return "", fmt.Errorf("failed to write to stdin: %w", err)
444+
return "", fmt.Errorf("failed to write request: %w", err)
409445
}
410-
_ = stdin.Close()
411446

412-
// Wait for the command to complete
413-
if err := cmd.Wait(); err != nil {
414-
return "", fmt.Errorf("command failed: %w, stderr: %s", err, stderr.String())
447+
// Step 5: Read the actual response (skip any server notifications)
448+
response, err := readJSONRPCResponse(scanner)
449+
if err != nil {
450+
return "", fmt.Errorf("failed to read response: %w, stderr: %s", err, stderr.String())
415451
}
416452

417-
return stdout.String(), nil
453+
return response, nil
454+
}
455+
456+
// buildInitializeRequest creates the MCP initialize handshake request.
457+
func buildInitializeRequest() (string, error) {
458+
id, err := rand.Int(rand.Reader, big.NewInt(10000))
459+
if err != nil {
460+
return "", fmt.Errorf("failed to generate random ID: %w", err)
461+
}
462+
msg := map[string]any{
463+
"jsonrpc": "2.0",
464+
"id": int(id.Int64()),
465+
"method": "initialize",
466+
"params": map[string]any{
467+
"protocolVersion": "2024-11-05",
468+
"capabilities": map[string]any{},
469+
"clientInfo": map[string]any{
470+
"name": "mcpcurl",
471+
"version": "0.1.0",
472+
},
473+
},
474+
}
475+
data, err := json.Marshal(msg)
476+
if err != nil {
477+
return "", fmt.Errorf("failed to marshal initialize request: %w", err)
478+
}
479+
return string(data), nil
480+
}
481+
482+
// buildInitializedNotification creates the MCP initialized notification.
483+
func buildInitializedNotification() string {
484+
return `{"jsonrpc":"2.0","method":"notifications/initialized"}`
485+
}
486+
487+
// readJSONRPCResponse reads lines from the scanner, skipping server-initiated
488+
// notifications (messages without an "id" field), and returns the first response.
489+
func readJSONRPCResponse(scanner *bufio.Scanner) (string, error) {
490+
for scanner.Scan() {
491+
line := scanner.Text()
492+
// JSON-RPC responses have an "id" field; notifications do not.
493+
var msg map[string]json.RawMessage
494+
if err := json.Unmarshal([]byte(line), &msg); err != nil {
495+
return "", fmt.Errorf("failed to parse JSON-RPC message: %w", err)
496+
}
497+
if _, hasID := msg["id"]; hasID {
498+
if errField, hasErr := msg["error"]; hasErr {
499+
return "", fmt.Errorf("server returned error: %s", string(errField))
500+
}
501+
return line, nil
502+
}
503+
// No "id" — this is a notification, skip it
504+
}
505+
if err := scanner.Err(); err != nil {
506+
return "", err
507+
}
508+
return "", fmt.Errorf("unexpected end of output")
418509
}
419510

420511
func printResponse(response string, prettyPrint bool) error {

0 commit comments

Comments
 (0)