This commit is contained in:
Shen Junzheng
2025-03-12 01:19:35 +08:00
parent 160040f3e1
commit 78cce92ce3
70 changed files with 10134 additions and 1 deletions

55
internal/mcp/error.go Normal file
View File

@@ -0,0 +1,55 @@
package mcp
import (
"fmt"
)
// enum ErrorCode {
// // Standard JSON-RPC error codes
// ParseError = -32700,
// InvalidRequest = -32600,
// MethodNotFound = -32601,
// InvalidParams = -32602,
// InternalError = -32603
// }
// Error
type Error struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
var (
ErrParseError = &Error{Code: -32700, Message: "Parse error"}
ErrInvalidRequest = &Error{Code: -32600, Message: "Invalid Request"}
ErrMethodNotFound = &Error{Code: -32601, Message: "Method not found"}
ErrInvalidParams = &Error{Code: -32602, Message: "Invalid params"}
ErrInternalError = &Error{Code: -32603, Message: "Internal error"}
ErrInvalidSessionID = &Error{Code: 400, Message: "Invalid session ID"}
ErrSessionNotFound = &Error{Code: 404, Message: "Could not find session"}
ErrTooManyRequests = &Error{Code: 429, Message: "Too many requests"}
)
func (e *Error) Error() string {
return fmt.Sprintf("%d: %s", e.Code, e.Message)
}
func (e *Error) JsonRPC() Response {
return Response{
JsonRPC: JsonRPCVersion,
Error: e,
}
}
func NewErrorResponse(id interface{}, code int, err error) *Response {
return &Response{
JsonRPC: JsonRPCVersion,
ID: id,
Error: &Error{
Code: code,
Message: err.Error(),
},
}
}

View File

@@ -0,0 +1,78 @@
package mcp
const (
MethodInitialize = "initialize"
MethodPing = "ping"
ProtocolVersion = "2024-11-05"
)
// {
// "method": "initialize",
// "params": {
// "protocolVersion": "2024-11-05",
// "capabilities": {
// "sampling": {},
// "roots": {
// "listChanged": true
// }
// },
// "clientInfo": {
// "name": "mcp-inspector",
// "version": "0.0.1"
// }
// },
// "jsonrpc": "2.0",
// "id": 0
// }
type InitializeRequest struct {
ProtocolVersion string `json:"protocolVersion"`
Capabilities M `json:"capabilities"`
ClientInfo *ClientInfo `json:"clientInfo"`
}
type ClientInfo struct {
Name string `json:"name"`
Version string `json:"version"`
}
// {
// "jsonrpc": "2.0",
// "id": 0,
// "result": {
// "protocolVersion": "2024-11-05",
// "capabilities": {
// "experimental": {},
// "prompts": {
// "listChanged": false
// },
// "resources": {
// "subscribe": false,
// "listChanged": false
// },
// "tools": {
// "listChanged": false
// }
// },
// "serverInfo": {
// "name": "weather",
// "version": "1.4.1"
// }
// }
// }
type InitializeResponse struct {
ProtocolVersion string `json:"protocolVersion"`
Capabilities M `json:"capabilities"`
ServerInfo ServerInfo `json:"serverInfo"`
}
type ServerInfo struct {
Name string `json:"name"`
Version string `json:"version"`
}
var DefaultCapabilities = M{
"experimental": M{},
"prompts": M{"listChanged": false},
"resources": M{"subscribe": false, "listChanged": false},
"tools": M{"listChanged": false},
}

62
internal/mcp/jsonrpc.go Normal file
View File

@@ -0,0 +1,62 @@
package mcp
const (
JsonRPCVersion = "2.0"
)
// Documents: https://modelcontextprotocol.io/docs/concepts/transports
// Request
//
// {
// jsonrpc: "2.0",
// id: number | string,
// method: string,
// params?: object
// }
type Request struct {
JsonRPC string `json:"jsonrpc"`
ID interface{} `json:"id"`
Method string `json:"method"`
Params interface{} `json:"params,omitempty"`
}
// Response
//
// {
// jsonrpc: "2.0",
// id: number | string,
// result?: object,
// error?: {
// code: number,
// message: string,
// data?: unknown
// }
// }
type Response struct {
JsonRPC string `json:"jsonrpc"`
ID interface{} `json:"id"`
Result interface{} `json:"result,omitempty"`
Error *Error `json:"error,omitempty"`
}
func NewResponse(id interface{}, result interface{}) *Response {
return &Response{
JsonRPC: JsonRPCVersion,
ID: id,
Result: result,
}
}
// Notifications
//
// {
// jsonrpc: "2.0",
// method: string,
// params?: object
// }
type Notification struct {
JsonRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params interface{} `json:"params,omitempty"`
}

107
internal/mcp/mcp.go Normal file
View File

@@ -0,0 +1,107 @@
package mcp
import (
"io"
"net/http"
"sync"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
)
const (
ProcessChanCap = 1000
)
type MCP struct {
sessions map[string]*Session
sessionMu sync.Mutex
ProcessChan chan ProcessCtx
}
func NewMCP() *MCP {
return &MCP{
sessions: make(map[string]*Session),
ProcessChan: make(chan ProcessCtx, ProcessChanCap),
}
}
func (m *MCP) HandleSSE(c *gin.Context) {
id := uuid.New().String()
m.sessionMu.Lock()
m.sessions[id] = NewSession(c, id)
m.sessionMu.Unlock()
c.Stream(func(w io.Writer) bool {
<-c.Request.Context().Done()
return false
})
m.sessionMu.Lock()
delete(m.sessions, id)
m.sessionMu.Unlock()
}
func (m *MCP) GetSession(id string) *Session {
m.sessionMu.Lock()
defer m.sessionMu.Unlock()
return m.sessions[id]
}
func (m *MCP) HandleMessages(c *gin.Context) {
// panic("xxx")
// 啊这, 一个 sessionid 有 3 种写法 session_id, sessionId, sessionid
// 官方 SDK 是 session_id: https://github.com/modelcontextprotocol/python-sdk/blob/c897868/src/mcp/server/sse.py#L98
// 写的是 sessionId: https://github.com/modelcontextprotocol/inspector/blob/aeaf32f/server/src/index.ts#L157
sessionID := c.Query("session_id")
if sessionID == "" {
sessionID = c.Query("sessionId")
}
if sessionID == "" {
sessionID = c.Param("sessionid")
}
if sessionID == "" {
c.JSON(http.StatusBadRequest, ErrInvalidSessionID.JsonRPC())
c.Abort()
return
}
session := m.GetSession(sessionID)
if session == nil {
c.JSON(http.StatusNotFound, ErrSessionNotFound.JsonRPC())
c.Abort()
return
}
var req Request
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ErrInvalidRequest.JsonRPC())
c.Abort()
return
}
log.Printf("收到消息: %v\n", req)
select {
case m.ProcessChan <- ProcessCtx{Session: session, Request: &req}:
default:
c.JSON(http.StatusTooManyRequests, ErrTooManyRequests.JsonRPC())
c.Abort()
return
}
c.String(http.StatusAccepted, "Accepted")
}
func (m *MCP) Close() {
close(m.ProcessChan)
}
type ProcessCtx struct {
Session *Session
Request *Request
}

137
internal/mcp/prompt.go Normal file
View File

@@ -0,0 +1,137 @@
package mcp
// Document: https://modelcontextprotocol.io/docs/concepts/prompts
const (
// Client => Server
MethodPromptsList = "prompts/list"
MethodPromptsGet = "prompts/get"
)
// Prompt
//
// {
// name: string; // Unique identifier for the prompt
// description?: string; // Human-readable description
// arguments?: [ // Optional list of arguments
// {
// name: string; // Argument identifier
// description?: string; // Argument description
// required?: boolean; // Whether argument is required
// }
// ]
// }
type Prompt struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Arguments []PromptArgument `json:"arguments,omitempty"`
}
type PromptArgument struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Required bool `json:"required,omitempty"`
}
// ListPrompts
//
// {
// prompts: [
// {
// name: "analyze-code",
// description: "Analyze code for potential improvements",
// arguments: [
// {
// name: "language",
// description: "Programming language",
// required: true
// }
// ]
// }
// ]
// }
type PromptsListResponse struct {
Prompts []Prompt `json:"prompts"`
}
// Use Prompt
// Request
//
// {
// method: "prompts/get",
// params: {
// name: "analyze-code",
// arguments: {
// language: "python"
// }
// }
// }
//
// Response
//
// {
// description: "Analyze Python code for potential improvements",
// messages: [
// {
// role: "user",
// content: {
// type: "text",
// text: "Please analyze the following Python code for potential improvements:\n\n```python\ndef calculate_sum(numbers):\n total = 0\n for num in numbers:\n total = total + num\n return total\n\nresult = calculate_sum([1, 2, 3, 4, 5])\nprint(result)\n```"
// }
// }
// ]
// }
type PromptsGetRequest struct {
Name string `json:"name"`
Arguments M `json:"arguments"`
}
type PromptsGetResponse struct {
Description string `json:"description"`
Messages []PromptMessage `json:"messages"`
}
type PromptMessage struct {
Role string `json:"role"`
Content PromptContent `json:"content"`
}
type PromptContent struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
Resource interface{} `json:"resource,omitempty"` // Resource or ResourceTemplate
}
// {
// "messages": [
// {
// "role": "user",
// "content": {
// "type": "text",
// "text": "Analyze these system logs and the code file for any issues:"
// }
// },
// {
// "role": "user",
// "content": {
// "type": "resource",
// "resource": {
// "uri": "logs://recent?timeframe=1h",
// "text": "[2024-03-14 15:32:11] ERROR: Connection timeout in network.py:127\n[2024-03-14 15:32:15] WARN: Retrying connection (attempt 2/3)\n[2024-03-14 15:32:20] ERROR: Max retries exceeded",
// "mimeType": "text/plain"
// }
// }
// },
// {
// "role": "user",
// "content": {
// "type": "resource",
// "resource": {
// "uri": "file:///path/to/code.py",
// "text": "def connect_to_service(timeout=30):\n retries = 3\n for attempt in range(retries):\n try:\n return establish_connection(timeout)\n except TimeoutError:\n if attempt == retries - 1:\n raise\n time.sleep(5)\n\ndef establish_connection(timeout):\n # Connection implementation\n pass",
// "mimeType": "text/x-python"
// }
// }
// }
// ]
// }

74
internal/mcp/resource.go Normal file
View File

@@ -0,0 +1,74 @@
package mcp
// Document: https://modelcontextprotocol.io/docs/concepts/resources
const (
// Client => Server
MethodResourcesList = "resources/list"
MethodResourcesTemplateList = "resources/templates/list"
MethodResourcesRead = "resources/read"
MethodResourcesSubscribe = "resources/subscribe"
MethodResourcesUnsubscribe = "resources/unsubscribe"
// Server => Client
NotificationResourcesListChanged = "notifications/resources/list_changed"
NofiticationResourcesUpdated = "notifications/resources/updated"
)
// Direct resources
//
// {
// uri: string; // Unique identifier for the resource
// name: string; // Human-readable name
// description?: string; // Optional description
// mimeType?: string; // Optional MIME type
// }
type Resource struct {
URI string `json:"uri"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
MimeType string `json:"mimeType,omitempty"`
}
// Resource templates
//
// {
// uriTemplate: string; // URI template following RFC 6570
// name: string; // Human-readable name for this type
// description?: string; // Optional description
// mimeType?: string; // Optional MIME type for all matching resources
// }
type ResourceTemplate struct {
URITemplate string `json:"uriTemplate"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
MimeType string `json:"mimeType,omitempty"`
}
// Reading resources
// {
// contents: [
// {
// uri: string; // The URI of the resource
// mimeType?: string; // Optional MIME type
// // One of:
// text?: string; // For text resources
// blob?: string; // For binary resources (base64 encoded)
// }
// ]
// }
type ReadingResource struct {
Contents []ReadingResourceContent `json:"contents"`
}
type ResourcesReadRequest struct {
URI string `json:"uri"`
}
type ReadingResourceContent struct {
URI string `json:"uri"`
MimeType string `json:"mimeType,omitempty"`
Text string `json:"text,omitempty"`
Blob string `json:"blob,omitempty"`
}

48
internal/mcp/session.go Normal file
View File

@@ -0,0 +1,48 @@
package mcp
import (
"encoding/json"
"io"
"github.com/gin-gonic/gin"
)
type Session struct {
id string
w io.Writer
c *ClientInfo
}
func NewSession(c *gin.Context, id string) *Session {
return &Session{
id: id,
w: NewSSEWriter(c, id),
}
}
func (s *Session) Write(p []byte) (n int, err error) {
return s.w.Write(p)
}
func (s *Session) WriteError(req *Request, err error) {
resp := NewErrorResponse(req.ID, 500, err)
b, err := json.Marshal(resp)
if err != nil {
return
}
s.Write(b)
}
func (s *Session) WriteResponse(req *Request, data interface{}) error {
resp := NewResponse(req.ID, data)
b, err := json.Marshal(resp)
if err != nil {
return err
}
s.Write(b)
return nil
}
func (s *Session) SaveClientInfo(c *ClientInfo) {
s.c = c
}

160
internal/mcp/sse.go Normal file
View File

@@ -0,0 +1,160 @@
package mcp
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
)
const (
SSEPingIntervalS = 30
SSEMessageChanCap = 100
SSEContentType = "text/event-stream; charset=utf-8"
)
type SSEWriter struct {
id string
c *gin.Context
}
func NewSSEWriter(c *gin.Context, id string) *SSEWriter {
c.Writer.Header().Set("Content-Type", SSEContentType)
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Flush()
w := &SSEWriter{
id: id,
c: c,
}
w.WriteEndpoing()
go w.ping()
return w
}
func (w *SSEWriter) Write(p []byte) (n int, err error) {
w.WriteMessage(string(p))
return len(p), nil
}
func (w *SSEWriter) WriteMessage(data string) {
w.WriteEvent("message", data)
}
func (w *SSEWriter) WriteEvent(event string, data string) {
w.c.Writer.WriteString(fmt.Sprintf("event: %s\n", event))
w.c.Writer.WriteString(fmt.Sprintf("data: %s\n\n", data))
w.c.Writer.Flush()
}
func (w *SSEWriter) ping() {
for {
select {
case <-time.After(time.Second * SSEPingIntervalS):
w.writePing()
case <-w.c.Request.Context().Done():
return
}
}
}
// WriteEndpoing
// event: endpoint
// data: /message?sessionId=285d67ee-1c17-40d9-ab03-173d5ff48419
func (w *SSEWriter) WriteEndpoing() {
w.c.Writer.WriteString(fmt.Sprintf("event: endpoint\n"))
w.c.Writer.WriteString(fmt.Sprintf("data: /message?sessionId=%s\n\n", w.id))
w.c.Writer.Flush()
}
// WritePing
// : ping - 2025-03-16 06:41:51.280928+00:00
func (w *SSEWriter) writePing() {
w.c.Writer.WriteString(fmt.Sprintf(": ping - %s\n\n", time.Now().Format("2006-01-02 15:04:05.999999-07:00")))
}
// SSE Session
// 维持一个 SSE 连接的会话
// 会话中包含了 SSE 连接的 ID事件通道停止通道
// 事件通道用于发送事件,停止通道用于停止会话
// 需要轮询发送 ping 事件以保持连接
type SSESession struct {
SessionID string
Events map[string]chan string
Stop chan bool
c *gin.Context
}
func NewSSESession(c *gin.Context) *SSESession {
return &SSESession{c: c}
}
func (s *SSESession) SendEvent(event string, data string) {
s.c.SSEvent(event, data)
}
func (s *SSESession) Close() {
close(s.Stop)
}
// Event
// request:
// POST /messages?sesessionId=?
// '{"method":"prompts/list","params":{},"jsonrpc":"2.0","id":3}'
//
// response:
// GET /sse
// event: message
// data: {"jsonrpc":"2.0","id":3,"result":{"prompts":[]}}
// {
// "jsonrpc": "2.0",
// "id": 1,
// "result": {
// "tools": [
// {
// "name": "get_alerts",
// "description": "Get weather alerts for a US state.\n\n Args:\n state: Two-letter US state code (e.g. CA, NY)\n ",
// "inputSchema": {
// "properties": {
// "state": {
// "title": "State",
// "type": "string"
// }
// },
// "required": [
// "state"
// ],
// "title": "get_alertsArguments",
// "type": "object"
// }
// },
// {
// "name": "get_forecast",
// "description": "Get weather forecast for a location.\n\n Args:\n latitude: Latitude of the location\n longitude: Longitude of the location\n ",
// "inputSchema": {
// "properties": {
// "latitude": {
// "title": "Latitude",
// "type": "number"
// },
// "longitude": {
// "title": "Longitude",
// "type": "number"
// }
// },
// "required": [
// "latitude",
// "longitude"
// ],
// "title": "get_forecastArguments",
// "type": "object"
// }
// }
// ]
// }
// }
// PING

1
internal/mcp/stdio.go Normal file
View File

@@ -0,0 +1 @@
package mcp

142
internal/mcp/tool.go Normal file
View File

@@ -0,0 +1,142 @@
package mcp
// Document: https://modelcontextprotocol.io/docs/concepts/tools
const (
// Client => Server
MethodToolsList = "tools/list"
MethodToolsCall = "tools/call"
)
type M map[string]interface{}
// Tool
//
// {
// name: string; // Unique identifier for the tool
// description?: string; // Human-readable description
// inputSchema: { // JSON Schema for the tool's parameters
// type: "object",
// properties: { ... } // Tool-specific parameters
// }
// }
//
// {
// name: "analyze_csv",
// description: "Analyze a CSV file",
// inputSchema: {
// type: "object",
// properties: {
// filepath: { type: "string" },
// operations: {
// type: "array",
// items: {
// enum: ["sum", "average", "count"]
// }
// }
// }
// }
// }
//
// {
// "jsonrpc": "2.0",
// "id": 1,
// "result": {
// "tools": [
// {
// "name": "get_alerts",
// "description": "Get weather alerts for a US state.\n\n Args:\n state: Two-letter US state code (e.g. CA, NY)\n ",
// "inputSchema": {
// "properties": {
// "state": {
// "title": "State",
// "type": "string"
// }
// },
// "required": [
// "state"
// ],
// "title": "get_alertsArguments",
// "type": "object"
// }
// },
// {
// "name": "get_forecast",
// "description": "Get weather forecast for a location.\n\n Args:\n latitude: Latitude of the location\n longitude: Longitude of the location\n ",
// "inputSchema": {
// "properties": {
// "latitude": {
// "title": "Latitude",
// "type": "number"
// },
// "longitude": {
// "title": "Longitude",
// "type": "number"
// }
// },
// "required": [
// "latitude",
// "longitude"
// ],
// "title": "get_forecastArguments",
// "type": "object"
// }
// }
// ]
// }
// }
type Tool struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
InputSchema ToolSchema `json:"inputSchema"`
}
type ToolSchema struct {
Type string `json:"type"`
Properties M `json:"properties"`
}
// {
// "method": "tools/call",
// "params": {
// "name": "chatlog",
// "arguments": {
// "start": "2006-11-12",
// "end": "2020-11-20",
// "limit": "50",
// "offset": "6"
// },
// "_meta": {
// "progressToken": 1
// }
// },
// "jsonrpc": "2.0",
// "id": 3
// }
type ToolsCallRequest struct {
Name string `json:"name"`
Arguments M `json:"arguments"`
}
// {
// "jsonrpc": "2.0",
// "id": 2,
// "result": {
// "content": [
// {
// "type": "text",
// "text": "\nEvent: Winter Storm Warning\n"
// }
// ],
// "isError": false
// }
// }
type ToolsCallResponse struct {
Content []Content `json:"content"`
IsError bool `json:"isError"`
}
type Content struct {
Type string `json:"type"`
Text string `json:"text"`
}