Skip to content

Commit 18c2be7

Browse files
committed
Merge remote-tracking branch 'upstream/main' into fix/issue_write-body-description-2410
2 parents b1ad4d6 + 457f599 commit 18c2be7

25 files changed

Lines changed: 1359 additions & 135 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,7 +1163,7 @@ The following sets of tools are available:
11631163
- `owner`: Repository owner (string, required)
11641164
- `pullNumber`: Pull request number to update (number, required)
11651165
- `repo`: Repository name (string, required)
1166-
- `reviewers`: GitHub usernames to request reviews from (string[], optional)
1166+
- `reviewers`: GitHub usernames or ORG/team-slug team reviewers to request reviews from (string[], optional)
11671167
- `state`: New state (string, optional)
11681168
- `title`: New title (string, optional)
11691169

@@ -1221,7 +1221,7 @@ The following sets of tools are available:
12211221

12221222
- **get_commit** - Get commit details
12231223
- **Required OAuth Scopes**: `repo`
1224-
- `include_diff`: Whether to include file diffs and stats in the response. Default is true. (boolean, optional)
1224+
- `detail`: Level of detail to include for changed files. "none" omits stats and files entirely. "stats" (default) includes per-file metadata: filename, status, and lines-of-code counts (additions, deletions, changes), with no patch content. "full_patch" additionally includes the unified diff content for each file and can be very large. (string, optional)
12251225
- `owner`: Repository owner (string, required)
12261226
- `page`: Page number for pagination (min 1) (number, optional)
12271227
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)

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 {

cmd/mcpcurl/main_test.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"encoding/json"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestReadJSONRPCResponse_DirectResponse(t *testing.T) {
11+
t.Parallel()
12+
input := `{"jsonrpc":"2.0","id":1,"result":{"tools":[]}}` + "\n"
13+
scanner := bufio.NewScanner(strings.NewReader(input))
14+
15+
got, err := readJSONRPCResponse(scanner)
16+
if err != nil {
17+
t.Fatalf("unexpected error: %v", err)
18+
}
19+
if got != `{"jsonrpc":"2.0","id":1,"result":{"tools":[]}}` {
20+
t.Fatalf("unexpected response: %s", got)
21+
}
22+
}
23+
24+
func TestReadJSONRPCResponse_SkipsNotifications(t *testing.T) {
25+
t.Parallel()
26+
input := strings.Join([]string{
27+
`{"jsonrpc":"2.0","method":"notifications/resources/list_changed","params":{}}`,
28+
`{"jsonrpc":"2.0","method":"notifications/tools/list_changed"}`,
29+
`{"jsonrpc":"2.0","id":42,"result":{"content":[{"type":"text","text":"hello"}]}}`,
30+
}, "\n") + "\n"
31+
scanner := bufio.NewScanner(strings.NewReader(input))
32+
33+
got, err := readJSONRPCResponse(scanner)
34+
if err != nil {
35+
t.Fatalf("unexpected error: %v", err)
36+
}
37+
38+
var msg map[string]json.RawMessage
39+
if err := json.Unmarshal([]byte(got), &msg); err != nil {
40+
t.Fatalf("response is not valid JSON: %v", err)
41+
}
42+
// Verify we got the response with id:42, not a notification
43+
var id int
44+
if err := json.Unmarshal(msg["id"], &id); err != nil {
45+
t.Fatalf("failed to parse id: %v", err)
46+
}
47+
if id != 42 {
48+
t.Fatalf("expected id 42, got %d", id)
49+
}
50+
}
51+
52+
func TestReadJSONRPCResponse_NoResponse(t *testing.T) {
53+
t.Parallel()
54+
// Only notifications, no response
55+
input := `{"jsonrpc":"2.0","method":"notifications/resources/list_changed","params":{}}` + "\n"
56+
scanner := bufio.NewScanner(strings.NewReader(input))
57+
58+
_, err := readJSONRPCResponse(scanner)
59+
if err == nil {
60+
t.Fatal("expected error for missing response, got nil")
61+
}
62+
if !strings.Contains(err.Error(), "unexpected end of output") {
63+
t.Fatalf("expected 'unexpected end of output' error, got: %v", err)
64+
}
65+
}
66+
67+
func TestReadJSONRPCResponse_EmptyInput(t *testing.T) {
68+
t.Parallel()
69+
scanner := bufio.NewScanner(strings.NewReader(""))
70+
71+
_, err := readJSONRPCResponse(scanner)
72+
if err == nil {
73+
t.Fatal("expected error for empty input, got nil")
74+
}
75+
}
76+
77+
func TestReadJSONRPCResponse_InvalidJSON(t *testing.T) {
78+
t.Parallel()
79+
input := "not valid json\n"
80+
scanner := bufio.NewScanner(strings.NewReader(input))
81+
82+
_, err := readJSONRPCResponse(scanner)
83+
if err == nil {
84+
t.Fatal("expected error for invalid JSON, got nil")
85+
}
86+
if !strings.Contains(err.Error(), "failed to parse JSON-RPC message") {
87+
t.Fatalf("expected parse error, got: %v", err)
88+
}
89+
}
90+
91+
func TestReadJSONRPCResponse_ServerError(t *testing.T) {
92+
t.Parallel()
93+
input := `{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"method not found"}}` + "\n"
94+
scanner := bufio.NewScanner(strings.NewReader(input))
95+
96+
_, err := readJSONRPCResponse(scanner)
97+
if err == nil {
98+
t.Fatal("expected error for server error response, got nil")
99+
}
100+
if !strings.Contains(err.Error(), "server returned error") {
101+
t.Fatalf("expected 'server returned error', got: %v", err)
102+
}
103+
if !strings.Contains(err.Error(), "method not found") {
104+
t.Fatalf("expected error to contain server message, got: %v", err)
105+
}
106+
}
107+
108+
func TestBuildInitializeRequest(t *testing.T) {
109+
t.Parallel()
110+
got, err := buildInitializeRequest()
111+
if err != nil {
112+
t.Fatalf("unexpected error: %v", err)
113+
}
114+
115+
var msg map[string]json.RawMessage
116+
if err := json.Unmarshal([]byte(got), &msg); err != nil {
117+
t.Fatalf("result is not valid JSON: %v", err)
118+
}
119+
120+
// Verify required fields
121+
for _, field := range []string{"jsonrpc", "id", "method", "params"} {
122+
if _, ok := msg[field]; !ok {
123+
t.Errorf("missing required field %q", field)
124+
}
125+
}
126+
127+
// Verify method
128+
var method string
129+
if err := json.Unmarshal(msg["method"], &method); err != nil {
130+
t.Fatalf("failed to parse method: %v", err)
131+
}
132+
if method != "initialize" {
133+
t.Errorf("expected method 'initialize', got %q", method)
134+
}
135+
136+
// Verify params contain protocolVersion and clientInfo
137+
var params map[string]json.RawMessage
138+
if err := json.Unmarshal(msg["params"], &params); err != nil {
139+
t.Fatalf("failed to parse params: %v", err)
140+
}
141+
for _, field := range []string{"protocolVersion", "capabilities", "clientInfo"} {
142+
if _, ok := params[field]; !ok {
143+
t.Errorf("missing params field %q", field)
144+
}
145+
}
146+
147+
var version string
148+
if err := json.Unmarshal(params["protocolVersion"], &version); err != nil {
149+
t.Fatalf("failed to parse protocolVersion: %v", err)
150+
}
151+
if version != "2024-11-05" {
152+
t.Errorf("expected protocolVersion '2024-11-05', got %q", version)
153+
}
154+
}
155+
156+
func TestBuildInitializedNotification(t *testing.T) {
157+
t.Parallel()
158+
got := buildInitializedNotification()
159+
160+
var msg map[string]json.RawMessage
161+
if err := json.Unmarshal([]byte(got), &msg); err != nil {
162+
t.Fatalf("result is not valid JSON: %v", err)
163+
}
164+
165+
// Must have jsonrpc and method
166+
var method string
167+
if err := json.Unmarshal(msg["method"], &method); err != nil {
168+
t.Fatalf("failed to parse method: %v", err)
169+
}
170+
if method != "notifications/initialized" {
171+
t.Errorf("expected method 'notifications/initialized', got %q", method)
172+
}
173+
174+
// Must NOT have an id (it's a notification)
175+
if _, hasID := msg["id"]; hasID {
176+
t.Error("notification should not have an 'id' field")
177+
}
178+
}

docs/feature-flags.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ runtime behavior (such as output formatting) won't appear here.
198198

199199
- **update_issue_type** - Update Issue Type
200200
- **Required OAuth Scopes**: `repo`
201+
- `confidence`: How confident you are in this choice. Use 'high' for clear signal or explicit user request, 'medium' for reasonable inference with some ambiguity, 'low' for best guess with limited signal. (string, optional)
201202
- `is_suggestion`: If true, this issue type change is sent to the API as a suggestion (suggest:true) rather than an applied value. Whether the type is applied or recorded as a proposal is determined by the API. (boolean, optional)
202203
- `issue_number`: The issue number to update (number, required)
203204
- `issue_type`: The issue type to set (string, required)
@@ -240,7 +241,7 @@ runtime behavior (such as output formatting) won't appear here.
240241
- `owner`: Repository owner (username or organization) (string, required)
241242
- `pullNumber`: The pull request number (number, required)
242243
- `repo`: Repository name (string, required)
243-
- `reviewers`: GitHub usernames to request reviews from (string[], required)
244+
- `reviewers`: GitHub usernames or ORG/team-slug team reviewers to request reviews from (string[], required)
244245

245246
- **resolve_review_thread** - Resolve Review Thread
246247
- **Required OAuth Scopes**: `repo`

pkg/github/__toolsnaps__/get_commit.snap

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@
66
"description": "Get details for a commit from a GitHub repository",
77
"inputSchema": {
88
"properties": {
9-
"include_diff": {
10-
"default": true,
11-
"description": "Whether to include file diffs and stats in the response. Default is true.",
12-
"type": "boolean"
9+
"detail": {
10+
"default": "stats",
11+
"description": "Level of detail to include for changed files. \"none\" omits stats and files entirely. \"stats\" (default) includes per-file metadata: filename, status, and lines-of-code counts (additions, deletions, changes), with no patch content. \"full_patch\" additionally includes the unified diff content for each file and can be very large.",
12+
"enum": [
13+
"none",
14+
"stats",
15+
"full_patch"
16+
],
17+
"type": "string"
1318
},
1419
"owner": {
1520
"description": "Repository owner",

pkg/github/__toolsnaps__/request_pull_request_reviewers.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"type": "string"
2222
},
2323
"reviewers": {
24-
"description": "GitHub usernames to request reviews from",
24+
"description": "GitHub usernames or ORG/team-slug team reviewers to request reviews from",
2525
"items": {
2626
"type": "string"
2727
},

0 commit comments

Comments
 (0)