Skip to content

Commit 0eab4b7

Browse files
authored
Feature/native mcp server 2 (#26)
1 parent 80a5b74 commit 0eab4b7

27 files changed

Lines changed: 1895 additions & 681 deletions

internal/adapters/mcp/executor_branches2_test.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package mcp
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
67
"testing"
78
"time"
@@ -385,17 +386,14 @@ func TestHandleToolCall_NilArguments(t *testing.T) {
385386
ctx := context.Background()
386387
s := newMockServer(&mockNylasClient{})
387388

389+
params, _ := json.Marshal(ToolCallParams{
390+
Name: "current_time",
391+
Arguments: nil,
392+
})
388393
req := &Request{
389394
JSONRPC: "2.0",
390395
Method: "tools/call",
391-
Params: struct {
392-
Name string `json:"name"`
393-
Arguments map[string]any `json:"arguments"`
394-
Cursor string `json:"cursor,omitempty"`
395-
}{
396-
Name: "current_time",
397-
Arguments: nil,
398-
},
396+
Params: params,
399397
}
400398

401399
raw := s.handleToolCall(ctx, req)
@@ -417,4 +415,3 @@ func TestHandleToolCall_NilArguments(t *testing.T) {
417415
t.Errorf("unexpected tool error with nil arguments: %s", text)
418416
}
419417
}
420-

internal/adapters/mcp/executor_messages.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,39 @@ package mcp
22

33
import (
44
"context"
5+
"strings"
56
"time"
67

78
"github.com/nylas/cli/internal/domain"
89
)
910

11+
// maxSnippetLen caps snippet length to reduce token usage in list responses.
12+
const maxSnippetLen = 120
13+
14+
// cleanSnippet removes invisible padding characters and trims to a reasonable length.
15+
func cleanSnippet(s string) string {
16+
// Remove zero-width non-joiners, zero-width spaces, and other invisible chars.
17+
s = strings.NewReplacer(
18+
"\u200c", "", // zero-width non-joiner (‌)
19+
"\u200b", "", // zero-width space
20+
"\u034f", "", // combining grapheme joiner (͏)
21+
"\r\n", " ",
22+
"\r", " ",
23+
"\n", " ",
24+
).Replace(s)
25+
26+
// Collapse multiple spaces.
27+
for strings.Contains(s, " ") {
28+
s = strings.ReplaceAll(s, " ", " ")
29+
}
30+
s = strings.TrimSpace(s)
31+
32+
if len(s) > maxSnippetLen {
33+
s = s[:maxSnippetLen] + "..."
34+
}
35+
return s
36+
}
37+
1038
// parseParticipants extracts email participants from tool arguments.
1139
// Accepts an array of objects with "email" and optional "name" fields.
1240
func parseParticipants(args map[string]any, key string) []domain.EmailParticipant {
@@ -84,9 +112,8 @@ func (s *Server) executeListMessages(ctx context.Context, args map[string]any) *
84112
"date": msg.Date.Format(time.RFC3339),
85113
"unread": msg.Unread,
86114
"starred": msg.Starred,
87-
"snippet": msg.Snippet,
115+
"snippet": cleanSnippet(msg.Snippet),
88116
"thread_id": msg.ThreadID,
89-
"folders": msg.Folders,
90117
})
91118
}
92119
return toolSuccess(result)

internal/adapters/mcp/handlers.go

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,38 @@ import (
66
"time"
77
)
88

9-
// Protocol version and server info.
9+
// Supported protocol versions (newest first).
10+
var supportedVersions = []string{
11+
"2025-03-26",
12+
"2024-11-05",
13+
}
14+
15+
// latestVersion is the most recent protocol version this server supports.
16+
const latestVersion = "2025-03-26"
17+
18+
// Server info constants.
1019
const (
11-
protocolVersion = "2024-11-05"
12-
serverName = "nylas-mcp"
13-
serverVersion = "1.0.0"
20+
serverName = "nylas-mcp"
21+
serverVersion = "1.0.0"
1422
)
1523

24+
// negotiateVersion returns the newest version both client and server support.
25+
// If the client's requested version is in our supported list, return it.
26+
// Otherwise, return the latest version we support.
27+
func negotiateVersion(requested string) string {
28+
for _, v := range supportedVersions {
29+
if v == requested {
30+
return v
31+
}
32+
}
33+
return latestVersion
34+
}
35+
1636
// handleInitialize responds to the MCP initialize request with server capabilities.
1737
func (s *Server) handleInitialize(req *Request) []byte {
38+
params := parseInitializeParams(req.Params)
39+
negotiated := negotiateVersion(params.ProtocolVersion)
40+
1841
// Detect user's timezone for guidance
1942
localZone, _ := time.Now().Zone()
2043
tzName := time.Local.String()
@@ -28,11 +51,11 @@ IMPORTANT - Timezone Consistency:
2851
The user's local timezone is: %s (%s)
2952
When displaying ANY timestamps to users (from emails, events, availability, etc.):
3053
1. Always use epoch_to_datetime tool with timezone "%s" to convert Unix timestamps
31-
2. Display ALL times in %s, never in UTC or the event's original timezone
54+
2. Display ALL times in %s, never in UTC
3255
3. Format times clearly (e.g., "2:00 PM %s")`, tzName, localZone, tzName, localZone, localZone)
3356

3457
result := map[string]any{
35-
"protocolVersion": protocolVersion,
58+
"protocolVersion": negotiated,
3659
"capabilities": map[string]any{
3760
"tools": map[string]any{},
3861
},
@@ -57,8 +80,9 @@ func (s *Server) handleToolsList(req *Request) []byte {
5780

5881
// handleToolCall dispatches a tool call to the appropriate executor.
5982
func (s *Server) handleToolCall(ctx context.Context, req *Request) []byte {
60-
name := req.Params.Name
61-
args := req.Params.Arguments
83+
params := parseToolCallParams(req.Params)
84+
name := params.Name
85+
args := params.Arguments
6286
if args == nil {
6387
args = make(map[string]any)
6488
}
@@ -183,7 +207,7 @@ func (s *Server) handleToolCall(ctx context.Context, req *Request) []byte {
183207
result = s.executeDatetimeToEpoch(args)
184208

185209
default:
186-
result = toolError(fmt.Sprintf("unknown tool: %s", name))
210+
return errorResponse(req.ID, codeInvalidParams, fmt.Sprintf("unknown tool: %s", name))
187211
}
188212

189213
return successResponse(req.ID, result)

internal/adapters/mcp/handlers_test.go

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -157,17 +157,15 @@ func parseToolResult(t *testing.T, resp jsonRPCResponse) toolResult {
157157

158158
// makeReq builds a minimal tools/call Request with the given tool name and args.
159159
func makeReq(name string, args map[string]any) *Request {
160+
params := ToolCallParams{
161+
Name: name,
162+
Arguments: args,
163+
}
164+
raw, _ := json.Marshal(params)
160165
return &Request{
161166
JSONRPC: "2.0",
162167
Method: "tools/call",
163-
Params: struct {
164-
Name string `json:"name"`
165-
Arguments map[string]any `json:"arguments"`
166-
Cursor string `json:"cursor,omitempty"`
167-
}{
168-
Name: name,
169-
Arguments: args,
170-
},
168+
Params: raw,
171169
}
172170
}
173171

@@ -310,24 +308,15 @@ func TestHandleToolCall_UnknownTool(t *testing.T) {
310308
req := makeReq("no_such_tool", map[string]any{})
311309
raw := s.handleToolCall(ctx, req)
312310

313-
// The dispatch always wraps in successResponse, so JSON-RPC level has no error.
311+
// Unknown tool now returns a JSON-RPC error with code -32602.
314312
rpc := parseRPCResponse(t, raw)
315-
if rpc.Error != nil {
316-
t.Fatalf("expected result (not JSON-RPC error) for unknown tool, got error: %s", rpc.Error.Message)
317-
}
318-
if rpc.Result == nil {
319-
t.Fatal("result field is missing")
320-
}
321-
322-
// The tool result itself should have isError=true with "unknown tool" message.
323-
tr := parseToolResult(t, rpc)
324-
if !tr.IsError {
325-
t.Error("expected isError=true for unknown tool, got isError=false")
313+
if rpc.Error == nil {
314+
t.Fatal("expected JSON-RPC error for unknown tool, got result")
326315
}
327-
if len(tr.Content) == 0 {
328-
t.Fatal("expected at least one content block in error response")
316+
if rpc.Error.Code != codeInvalidParams {
317+
t.Errorf("error.code = %d, want %d", rpc.Error.Code, codeInvalidParams)
329318
}
330-
if !strings.Contains(tr.Content[0].Text, "unknown tool") {
331-
t.Errorf("expected error text to contain 'unknown tool', got: %s", tr.Content[0].Text)
319+
if !strings.Contains(rpc.Error.Message, "unknown tool") {
320+
t.Errorf("expected error message to contain 'unknown tool', got: %s", rpc.Error.Message)
332321
}
333322
}

0 commit comments

Comments
 (0)