Commit b9c1e47
Changed files (9)
pkg/fetch/server.go
@@ -13,14 +13,6 @@ import (
"github.com/xlgmokha/mcp/pkg/mcp"
)
-// Server implements the Fetch MCP server
-type Server struct {
- *mcp.Server
- httpClient *http.Client
- userAgent string
- htmlProcessor *htmlprocessor.ContentExtractor
-}
-
// FetchResult represents the result of a fetch operation
type FetchResult struct {
URL string `json:"url"`
@@ -31,231 +23,195 @@ type FetchResult struct {
NextIndex int `json:"next_index,omitempty"`
}
-// New creates a new Fetch MCP server
-func New() *Server {
- server := mcp.NewServer("mcp-fetch", "1.0.0", []mcp.Tool{}, []mcp.Resource{}, []mcp.Root{})
+// FetchOperations provides HTTP client operations for fetching content
+type FetchOperations struct {
+ httpClient *http.Client
+ userAgent string
+ htmlProcessor *htmlprocessor.ContentExtractor
+}
- fetchServer := &Server{
- Server: server,
+// NewFetchOperations creates a new FetchOperations helper
+func NewFetchOperations() *FetchOperations {
+ return &FetchOperations{
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
userAgent: "ModelContextProtocol/1.0 (Fetch; +https://github.com/xlgmokha/mcp)",
htmlProcessor: htmlprocessor.NewContentExtractor(),
}
-
- // Register all fetch tools and prompts
- fetchServer.registerTools()
- fetchServer.registerPrompts()
-
- return fetchServer
}
-// registerTools registers all Fetch tools with the server
-func (fs *Server) registerTools() {
- // Get all tool definitions from ListTools method
- tools := fs.ListTools()
-
- // Register each tool with its proper definition
- for _, tool := range tools {
- var handler mcp.ToolHandler
- switch tool.Name {
- case "fetch":
- handler = fs.HandleFetch
- default:
- continue
- }
- fs.RegisterToolWithDefinition(tool, handler)
- }
-}
-
-// registerPrompts registers all Fetch prompts with the server
-func (fs *Server) registerPrompts() {
- fetchPrompt := mcp.Prompt{
- Name: "fetch",
- Description: "Prompt for manually entering a URL to fetch content from",
- Arguments: []mcp.PromptArgument{
- {
- Name: "url",
- Description: "The URL to fetch content from",
- Required: true,
+// New creates a new Fetch MCP server
+func New() *mcp.Server {
+ fetch := NewFetchOperations()
+ builder := mcp.NewServerBuilder("mcp-fetch", "1.0.0")
+
+ // Add fetch tool
+ builder.AddTool(mcp.NewTool("fetch", "Fetches a URL from the internet and extracts its contents as markdown. Always returns successful response with content or error details.", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "url": map[string]interface{}{
+ "type": "string",
+ "description": "URL to fetch",
+ "format": "uri",
},
- {
- Name: "reason",
- Description: "Why you want to fetch this URL (optional context)",
- Required: false,
+ "max_length": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum number of characters to return. Defaults to 5000",
+ "minimum": 1,
+ "maximum": 999999,
+ "default": 5000,
},
- },
- }
-
- fs.RegisterPrompt(fetchPrompt, fs.HandleFetchPrompt)
-}
-
-// ListTools returns all available Fetch tools
-func (fs *Server) ListTools() []mcp.Tool {
- return []mcp.Tool{
- {
- Name: "fetch",
- Description: "Fetches a URL from the internet and extracts its contents as markdown. Always returns successful response with content or error details.",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "url": map[string]interface{}{
- "type": "string",
- "description": "URL to fetch",
- "format": "uri",
- },
- "max_length": map[string]interface{}{
- "type": "integer",
- "description": "Maximum number of characters to return. Defaults to 5000",
- "minimum": 1,
- "maximum": 999999,
- "default": 5000,
- },
- "start_index": map[string]interface{}{
- "type": "integer",
- "description": "Start reading content from this character index. Defaults to 0",
- "minimum": 0,
- "default": 0,
- },
- "raw": map[string]interface{}{
- "type": "boolean",
- "description": "Get raw HTML content without markdown conversion. Defaults to false",
- "default": false,
- },
- },
- "required": []string{"url"},
+ "start_index": map[string]interface{}{
+ "type": "integer",
+ "description": "Start reading content from this character index. Defaults to 0",
+ "minimum": 0,
+ "default": 0,
+ },
+ "raw": map[string]interface{}{
+ "type": "boolean",
+ "description": "Get raw HTML content without markdown conversion. Defaults to false",
+ "default": false,
},
},
- }
-}
-
-// Tool handlers
-
-func (fs *Server) HandleFetch(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- urlStr, ok := req.Arguments["url"].(string)
- if !ok {
- return mcp.NewToolError("url is required"), nil
- }
-
- // Parse and validate URL
- parsedURL, err := url.Parse(urlStr)
- if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" {
- return mcp.NewToolError("Invalid URL format"), nil
- }
-
- // Get optional parameters
- maxLength := 5000
- if ml, ok := req.Arguments["max_length"]; ok {
- switch v := ml.(type) {
- case float64:
- maxLength = int(v)
- case int:
- maxLength = v
- default:
- return mcp.NewToolError("max_length must be a number"), nil
- }
- if maxLength < 1 || maxLength > 999999 {
- return mcp.NewToolError("max_length must be between 1 and 999999"), nil
+ "required": []string{"url"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ urlStr, ok := req.Arguments["url"].(string)
+ if !ok {
+ return mcp.NewToolError("url is required"), nil
}
- }
- startIndex := 0
- if si, ok := req.Arguments["start_index"]; ok {
- switch v := si.(type) {
- case float64:
- startIndex = int(v)
- case int:
- startIndex = v
- default:
- return mcp.NewToolError("start_index must be a number"), nil
+ // Parse and validate URL
+ parsedURL, err := url.Parse(urlStr)
+ if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" {
+ return mcp.NewToolError("Invalid URL format"), nil
}
- if startIndex < 0 {
- return mcp.NewToolError("start_index must be >= 0"), nil
+
+ // Get optional parameters
+ maxLength := 5000
+ if ml, ok := req.Arguments["max_length"]; ok {
+ switch v := ml.(type) {
+ case float64:
+ maxLength = int(v)
+ case int:
+ maxLength = v
+ default:
+ return mcp.NewToolError("max_length must be a number"), nil
+ }
+ if maxLength < 1 || maxLength > 999999 {
+ return mcp.NewToolError("max_length must be between 1 and 999999"), nil
+ }
}
- }
- raw := false
- if r, ok := req.Arguments["raw"]; ok {
- if rBool, ok := r.(bool); ok {
- raw = rBool
+ startIndex := 0
+ if si, ok := req.Arguments["start_index"]; ok {
+ switch v := si.(type) {
+ case float64:
+ startIndex = int(v)
+ case int:
+ startIndex = v
+ default:
+ return mcp.NewToolError("start_index must be a number"), nil
+ }
+ if startIndex < 0 {
+ return mcp.NewToolError("start_index must be >= 0"), nil
+ }
}
- }
- // Fetch the content
- result, err := fs.fetchContent(parsedURL.String(), maxLength, startIndex, raw)
- if err != nil {
- return mcp.NewToolError(err.Error()), nil
- }
+ raw := false
+ if r, ok := req.Arguments["raw"]; ok {
+ if rBool, ok := r.(bool); ok {
+ raw = rBool
+ }
+ }
- // Format result as JSON
- jsonResult, err := json.MarshalIndent(result, "", " ")
- if err != nil {
- return mcp.NewToolError(fmt.Sprintf("Failed to marshal result: %v", err)), nil
- }
+ // Fetch the content
+ result, err := fetch.fetchContent(parsedURL.String(), maxLength, startIndex, raw)
+ if err != nil {
+ return mcp.NewToolError(err.Error()), nil
+ }
- return mcp.NewToolResult(mcp.NewTextContent(string(jsonResult))), nil
-}
+ // Format result as JSON
+ jsonResult, err := json.MarshalIndent(result, "", " ")
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to marshal result: %v", err)), nil
+ }
-// Prompt handlers
+ return mcp.NewToolResult(mcp.NewTextContent(string(jsonResult))), nil
+ }))
-func (fs *Server) HandleFetchPrompt(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
- url, hasURL := req.Arguments["url"].(string)
- reason, hasReason := req.Arguments["reason"].(string)
+ // Add fetch prompt
+ builder.AddPrompt(mcp.NewPrompt("fetch", "Prompt for manually entering a URL to fetch content from", []mcp.PromptArgument{
+ {
+ Name: "url",
+ Description: "The URL to fetch content from",
+ Required: true,
+ },
+ {
+ Name: "reason",
+ Description: "Why you want to fetch this URL (optional context)",
+ Required: false,
+ },
+ }, func(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
+ url, hasURL := req.Arguments["url"].(string)
+ reason, hasReason := req.Arguments["reason"].(string)
- if !hasURL || url == "" {
- return mcp.GetPromptResult{}, fmt.Errorf("url argument is required")
- }
+ if !hasURL || url == "" {
+ return mcp.GetPromptResult{}, fmt.Errorf("url argument is required")
+ }
- // Create the prompt messages
- var messages []mcp.PromptMessage
+ // Create the prompt messages
+ var messages []mcp.PromptMessage
- // User message with the URL and optional reason
- userContent := fmt.Sprintf("Please fetch the content from this URL: %s", url)
- if hasReason && reason != "" {
- userContent += fmt.Sprintf("\n\nReason: %s", reason)
- }
+ // User message with the URL and optional reason
+ userContent := fmt.Sprintf("Please fetch the content from this URL: %s", url)
+ if hasReason && reason != "" {
+ userContent += fmt.Sprintf("\n\nReason: %s", reason)
+ }
- messages = append(messages, mcp.PromptMessage{
- Role: "user",
- Content: mcp.NewTextContent(userContent),
- })
+ messages = append(messages, mcp.PromptMessage{
+ Role: "user",
+ Content: mcp.NewTextContent(userContent),
+ })
- // Assistant message suggesting the fetch tool usage
- assistantContent := fmt.Sprintf(`I'll fetch the content from %s for you.
+ // Assistant message suggesting the fetch tool usage
+ assistantContent := fmt.Sprintf(`I'll fetch the content from %s for you.
Let me use the fetch tool to retrieve and process the content:`, url)
- messages = append(messages, mcp.PromptMessage{
- Role: "assistant",
- Content: mcp.NewTextContent(assistantContent),
- })
+ messages = append(messages, mcp.PromptMessage{
+ Role: "assistant",
+ Content: mcp.NewTextContent(assistantContent),
+ })
- description := "Manual URL fetch prompt"
- if hasReason && reason != "" {
- description = fmt.Sprintf("Manual URL fetch: %s", reason)
- }
+ description := "Manual URL fetch prompt"
+ if hasReason && reason != "" {
+ description = fmt.Sprintf("Manual URL fetch: %s", reason)
+ }
+
+ return mcp.GetPromptResult{
+ Description: description,
+ Messages: messages,
+ }, nil
+ }))
- return mcp.GetPromptResult{
- Description: description,
- Messages: messages,
- }, nil
+ return builder.Build()
}
-// Helper methods
+// Helper methods for FetchOperations
-func (fs *Server) fetchContent(urlStr string, maxLength, startIndex int, raw bool) (*FetchResult, error) {
+func (fetch *FetchOperations) fetchContent(urlStr string, maxLength, startIndex int, raw bool) (*FetchResult, error) {
// Create HTTP request
req, err := http.NewRequest("GET", urlStr, nil)
if err != nil {
return nil, fmt.Errorf("Failed to create request: %v", err)
}
- req.Header.Set("User-Agent", fs.userAgent)
+ req.Header.Set("User-Agent", fetch.userAgent)
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
// Perform HTTP request
- resp, err := fs.httpClient.Do(req)
+ resp, err := fetch.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("Failed to fetch URL: %v", err)
}
@@ -282,7 +238,7 @@ func (fs *Server) fetchContent(urlStr string, maxLength, startIndex int, raw boo
} else {
// Convert HTML to markdown using improved processor
var err error
- content, err = fs.htmlProcessor.ToMarkdown(string(body))
+ content, err = fetch.htmlProcessor.ToMarkdown(string(body))
if err != nil {
// Fallback to raw content if markdown conversion fails
content = string(body)
pkg/filesystem/server.go
@@ -12,111 +12,112 @@ import (
func New(allowedDirs []string) *mcp.Server {
tree := NewTree(allowedDirs)
- tools := []mcp.Tool{
- mcp.NewTool("read_file", "Read the contents of a file", map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "path": map[string]interface{}{
- "type": "string",
- },
+ builder := mcp.NewServerBuilder("filesystem", "0.2.0")
+
+ // Add tools
+ builder.AddTool(mcp.NewTool("read_file", "Read the contents of a file", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "path": map[string]interface{}{
+ "type": "string",
},
- "required": []string{"path"},
- }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- pathStr, ok := req.Arguments["path"].(string)
- if !ok {
- return mcp.NewToolError("path is required"), nil
- }
+ },
+ "required": []string{"path"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ pathStr, ok := req.Arguments["path"].(string)
+ if !ok {
+ return mcp.NewToolError("path is required"), nil
+ }
- validPath, err := tree.validatePath(pathStr)
- if err != nil {
- return mcp.NewToolError(err.Error()), nil
- }
+ validPath, err := tree.validatePath(pathStr)
+ if err != nil {
+ return mcp.NewToolError(err.Error()), nil
+ }
- content, err := os.ReadFile(validPath)
- if err != nil {
- return mcp.NewToolError(fmt.Sprintf("Failed to read file: %v", err)), nil
- }
+ content, err := os.ReadFile(validPath)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to read file: %v", err)), nil
+ }
- return mcp.NewToolResult(mcp.NewTextContent(string(content))), nil
- }),
+ return mcp.NewToolResult(mcp.NewTextContent(string(content))), nil
+ }))
- mcp.NewTool("write_file", "Write content to a file", map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "path": map[string]interface{}{
- "type": "string",
- },
- "content": map[string]interface{}{
- "type": "string",
- },
+ builder.AddTool(mcp.NewTool("write_file", "Write content to a file", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "path": map[string]interface{}{
+ "type": "string",
},
- "required": []string{"path", "content"},
- }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- pathStr, ok := req.Arguments["path"].(string)
- if !ok {
- return mcp.NewToolError("path is required"), nil
- }
+ "content": map[string]interface{}{
+ "type": "string",
+ },
+ },
+ "required": []string{"path", "content"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ pathStr, ok := req.Arguments["path"].(string)
+ if !ok {
+ return mcp.NewToolError("path is required"), nil
+ }
+
+ content, ok := req.Arguments["content"].(string)
+ if !ok {
+ return mcp.NewToolError("content is required"), nil
+ }
- content, ok := req.Arguments["content"].(string)
- if !ok {
- return mcp.NewToolError("content is required"), nil
+ validPath, err := tree.validatePath(pathStr)
+ if err != nil {
+ return mcp.NewToolError(err.Error()), nil
+ }
+
+ err = os.WriteFile(validPath, []byte(content), 0644)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to write file: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Successfully wrote to %s", pathStr))), nil
+ }))
+
+ // Add pattern resource for file:// scheme
+ builder.AddResource(mcp.NewResource(
+ "file://",
+ "File System",
+ "",
+ func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
+ if !strings.HasPrefix(req.URI, "file://") {
+ return mcp.ReadResourceResult{}, fmt.Errorf("invalid file URI: %s", req.URI)
}
- validPath, err := tree.validatePath(pathStr)
+ filePath := req.URI[7:]
+ validPath, err := tree.validatePath(filePath)
if err != nil {
- return mcp.NewToolError(err.Error()), nil
+ return mcp.ReadResourceResult{}, fmt.Errorf("access denied: %v", err)
}
- err = os.WriteFile(validPath, []byte(content), 0644)
+ content, err := os.ReadFile(validPath)
if err != nil {
- return mcp.NewToolError(fmt.Sprintf("Failed to write file: %v", err)), nil
+ return mcp.ReadResourceResult{}, fmt.Errorf("failed to read file: %v", err)
}
- return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Successfully wrote to %s", pathStr))), nil
- }),
- }
-
- resources := []mcp.Resource{
- mcp.NewResource(
- "file://",
- "File System",
- "",
- func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
- if !strings.HasPrefix(req.URI, "file://") {
- return mcp.ReadResourceResult{}, fmt.Errorf("invalid file URI: %s", req.URI)
- }
-
- filePath := req.URI[7:]
- validPath, err := tree.validatePath(filePath)
- if err != nil {
- return mcp.ReadResourceResult{}, fmt.Errorf("access denied: %v", err)
- }
-
- content, err := os.ReadFile(validPath)
- if err != nil {
- return mcp.ReadResourceResult{}, fmt.Errorf("failed to read file: %v", err)
- }
-
- if isBinaryContent(content) {
- return mcp.ReadResourceResult{
- Contents: []mcp.Content{
- mcp.TextContent{
- Type: "text",
- Text: fmt.Sprintf("Binary file (size: %d bytes)", len(content)),
- },
- },
- }, nil
- }
-
+ if isBinaryContent(content) {
return mcp.ReadResourceResult{
Contents: []mcp.Content{
- mcp.NewTextContent(string(content)),
+ mcp.TextContent{
+ Type: "text",
+ Text: fmt.Sprintf("Binary file (size: %d bytes)", len(content)),
+ },
},
}, nil
- },
- ),
- }
+ }
+
+ return mcp.ReadResourceResult{
+ Contents: []mcp.Content{
+ mcp.NewTextContent(string(content)),
+ },
+ }, nil
+ },
+ ))
+ // Add directory resources and discover files
for _, dir := range tree.Directories {
fileURI := "file://" + dir
dirName := filepath.Base(dir)
@@ -124,7 +125,7 @@ func New(allowedDirs []string) *mcp.Server {
dirName = dir
}
- resources = append(resources, mcp.NewResource(
+ builder.AddResource(mcp.NewResource(
fileURI,
fmt.Sprintf("Directory: %s", dirName),
"inode/directory",
@@ -139,18 +140,20 @@ func New(allowedDirs []string) *mcp.Server {
// Discover files in this directory at construction time
fileResources := tree.discoverFiles(dir)
- resources = append(resources, fileResources...)
+ for _, resource := range fileResources {
+ builder.AddResource(resource)
+ }
}
- var roots []mcp.Root
+ // Add roots
for _, dir := range tree.Directories {
fileURI := "file://" + dir
dirName := filepath.Base(dir)
if dirName == "." || dirName == "/" {
dirName = dir
}
- roots = append(roots, mcp.NewRoot(fileURI, fmt.Sprintf("Directory: %s", dirName)))
+ builder.AddRoot(mcp.NewRoot(fileURI, fmt.Sprintf("Directory: %s", dirName)))
}
- return mcp.NewServer("filesystem", "0.2.0", tools, resources, roots)
+ return builder.Build()
}
pkg/filesystem/tree.go
@@ -88,11 +88,14 @@ func (fs *Tree) discoverFiles(dirPath string) []mcp.Resource {
fileURI := "file://" + path
relPath, _ := filepath.Rel(dirPath, path)
- resource := mcp.Resource{
- URI: fileURI,
- Name: relPath,
- MimeType: mimeType,
- }
+ resource := mcp.NewResource(
+ fileURI,
+ relPath,
+ mimeType,
+ func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
+ return mcp.ReadResourceResult{}, fmt.Errorf("individual file resources are handled by the pattern handler")
+ },
+ )
resources = append(resources, resource)
fileCount++
pkg/git/server.go
@@ -1,7 +1,6 @@
package git
import (
- "encoding/json"
"fmt"
"os"
"os/exec"
@@ -12,259 +11,583 @@ import (
"github.com/xlgmokha/mcp/pkg/mcp"
)
-// Server implements the Git MCP server
-type Server struct {
- *mcp.Server
+// Commit represents a git commit
+type Commit struct {
+ Hash string
+ Message string
+ Author string
+ Date string
+}
+
+// GitOperations provides git operations for a specific repository
+type GitOperations struct {
repoPath string
}
+// NewGitOperations creates a new GitOperations helper
+func NewGitOperations(repoPath string) *GitOperations {
+ return &GitOperations{repoPath: repoPath}
+}
+
// New creates a new Git MCP server
-func New(repoPath string) *Server {
- server := mcp.NewServer("mcp-git", "1.0.0", []mcp.Tool{}, []mcp.Resource{}, []mcp.Root{})
+func New(repoPath string) *mcp.Server {
+ git := NewGitOperations(repoPath)
+ builder := mcp.NewServerBuilder("mcp-git", "1.0.0")
+
+ // Add git_status tool
+ builder.AddTool(mcp.NewTool("git_status", "Shows the working tree status", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ },
+ "required": []string{"repo_path"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ repoPath = git.repoPath
+ }
- gitServer := &Server{
- Server: server,
- repoPath: repoPath,
- }
+ output, err := git.runGitCommand(repoPath, "status")
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git status failed: %v", err)), nil
+ }
- // Register all git tools, prompts, resources, and roots
- gitServer.registerTools()
- gitServer.registerPrompts()
- gitServer.registerResources()
- gitServer.registerRoots()
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Repository status:\n%s", output))), nil
+ }))
- // Set up dynamic resource listing
- gitServer.setupResourceHandling()
+ // Add git_diff_unstaged tool
+ builder.AddTool(mcp.NewTool("git_diff_unstaged", "Shows changes in the working directory that are not yet staged", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ },
+ "required": []string{"repo_path"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ repoPath = git.repoPath
+ }
- return gitServer
-}
+ output, err := git.runGitCommand(repoPath, "diff")
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git diff failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Unstaged changes:\n%s", output))), nil
+ }))
-// setupResourceHandling configures custom resource handling for lazy loading
-func (gs *Server) setupResourceHandling() {
- // Custom handler that calls our ListResources method
- customListResourcesHandler := func(req mcp.JSONRPCRequest) mcp.JSONRPCResponse {
- resources := gs.ListResources()
- result := mcp.ListResourcesResult{Resources: resources}
- id := req.ID
- bytes, _ := json.Marshal(result)
- rawMsg := json.RawMessage(bytes)
- resultBytes := &rawMsg
- return mcp.JSONRPCResponse{
- JSONRPC: "2.0",
- ID: id,
- Result: resultBytes,
+ // Add git_diff_staged tool
+ builder.AddTool(mcp.NewTool("git_diff_staged", "Shows changes that are staged for commit", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ },
+ "required": []string{"repo_path"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ repoPath = git.repoPath
}
- }
-
- handlers := make(map[string]func(mcp.JSONRPCRequest) mcp.JSONRPCResponse)
- handlers["resources/list"] = customListResourcesHandler
- gs.SetCustomRequestHandler(handlers)
-}
-// ListResources dynamically discovers and returns git resources
-func (gs *Server) ListResources() []mcp.Resource {
- resources := make([]mcp.Resource, 0)
+ output, err := git.runGitCommand(repoPath, "diff", "--cached")
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git diff --cached failed: %v", err)), nil
+ }
- // Check if this is a git repository
- gitDir := filepath.Join(gs.repoPath, ".git")
- if _, err := os.Stat(gitDir); os.IsNotExist(err) {
- return resources // Return empty for non-git repositories
- }
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Staged changes:\n%s", output))), nil
+ }))
- // Get current branch (only when needed)
- currentBranch, err := gs.getCurrentBranch()
- if err != nil {
- currentBranch = "unknown"
- }
+ // Add git_diff tool
+ builder.AddTool(mcp.NewTool("git_diff", "Shows differences between branches or commits", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ "target": map[string]interface{}{
+ "type": "string",
+ "description": "Target branch or commit to diff against",
+ },
+ },
+ "required": []string{"repo_path", "target"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ repoPath = git.repoPath
+ }
- // Get tracked files (only when requested)
- trackedFiles, err := gs.getTrackedFiles()
- if err != nil {
- return resources // Return empty on error
- }
+ target, ok := req.Arguments["target"].(string)
+ if !ok {
+ return mcp.NewToolError("target is required"), nil
+ }
- // Create resources for tracked files (limited to first 500 for performance)
- limit := 500
- for i, filePath := range trackedFiles {
- if i >= limit {
- break // Prevent loading too many resources at once
+ output, err := git.runGitCommand(repoPath, "diff", target)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git diff failed: %v", err)), nil
}
- // Create git:// URI: git://repo/branch/path
- gitURI := fmt.Sprintf("git://%s/%s/%s", gs.repoPath, currentBranch, filePath)
-
- // Determine MIME type
- mimeType := getGitMimeType(filePath)
-
- // Create resource definition
- resource := mcp.Resource{
- URI: gitURI,
- Name: filepath.Base(filePath),
- Description: fmt.Sprintf("Git file: %s (branch: %s)", filePath, currentBranch),
- MimeType: mimeType,
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Diff with %s:\n%s", target, output))), nil
+ }))
+
+ // Add git_commit tool
+ builder.AddTool(mcp.NewTool("git_commit", "Records changes to the repository", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ "message": map[string]interface{}{
+ "type": "string",
+ "description": "Commit message",
+ },
+ },
+ "required": []string{"repo_path", "message"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ repoPath = git.repoPath
}
- resources = append(resources, resource)
- }
- // Add branch resources (limited set)
- branches, err := gs.getBranches()
- if err == nil {
- for i, branch := range branches {
- if i >= 10 { // Limit to 10 branches
- break
- }
- branchURI := fmt.Sprintf("git://%s/branch/%s", gs.repoPath, branch)
- resource := mcp.Resource{
- URI: branchURI,
- Name: fmt.Sprintf("Branch: %s", branch),
- Description: fmt.Sprintf("Git branch: %s", branch),
- MimeType: "application/x-git-branch",
- }
- resources = append(resources, resource)
+ message, ok := req.Arguments["message"].(string)
+ if !ok {
+ return mcp.NewToolError("message is required"), nil
+ }
+
+ output, err := git.runGitCommand(repoPath, "commit", "-m", message)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git commit failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Changes committed successfully:\n%s", output))), nil
+ }))
+
+ // Add git_add tool
+ builder.AddTool(mcp.NewTool("git_add", "Adds file contents to the staging area", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ "files": map[string]interface{}{
+ "type": "array",
+ "items": map[string]interface{}{
+ "type": "string",
+ },
+ "description": "List of files to add",
+ },
+ },
+ "required": []string{"repo_path", "files"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ repoPath = git.repoPath
+ }
+
+ filesInterface, ok := req.Arguments["files"]
+ if !ok {
+ return mcp.NewToolError("files is required"), nil
+ }
+
+ files, err := convertToStringSlice(filesInterface)
+ if err != nil {
+ return mcp.NewToolError("files must be an array of strings"), nil
}
- }
- // Add recent commit resources (limited set)
- commits, err := gs.getRecentCommits(10)
- if err == nil {
- for _, commit := range commits {
- commitURI := fmt.Sprintf("git://%s/commit/%s", gs.repoPath, commit.Hash)
- resource := mcp.Resource{
- URI: commitURI,
- Name: fmt.Sprintf("Commit: %s", commit.Hash[:8]),
- Description: fmt.Sprintf("Git commit: %s - %s", commit.Hash[:8], commit.Message),
- MimeType: "application/x-git-commit",
+ args := append([]string{"add"}, files...)
+ _, err = git.runGitCommand(repoPath, args...)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git add failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent("Files staged successfully")), nil
+ }))
+
+ // Add git_reset tool
+ builder.AddTool(mcp.NewTool("git_reset", "Unstages all staged changes", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ },
+ "required": []string{"repo_path"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ repoPath = git.repoPath
+ }
+
+ _, err := git.runGitCommand(repoPath, "reset")
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git reset failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent("All staged changes reset")), nil
+ }))
+
+ // Add git_log tool
+ builder.AddTool(mcp.NewTool("git_log", "Shows the commit logs", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ "max_count": map[string]interface{}{
+ "type": "integer",
+ "description": "Maximum number of commits to show",
+ "default": 10,
+ },
+ },
+ "required": []string{"repo_path"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ repoPath = git.repoPath
+ }
+
+ maxCount := 10
+ if mc, exists := req.Arguments["max_count"]; exists {
+ if count, ok := mc.(float64); ok {
+ maxCount = int(count)
}
- resources = append(resources, resource)
}
- }
- return resources
-}
+ output, err := git.runGitCommand(repoPath, "log", "--oneline", "-n", strconv.Itoa(maxCount))
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git log failed: %v", err)), nil
+ }
-// registerTools registers all Git tools with the server
-func (gs *Server) registerTools() {
- // Get all tool definitions from ListTools method
- tools := gs.ListTools()
-
- // Register each tool with its proper definition
- for _, tool := range tools {
- var handler mcp.ToolHandler
- switch tool.Name {
- case "git_status":
- handler = gs.HandleGitStatus
- case "git_diff_unstaged":
- handler = gs.HandleGitDiffUnstaged
- case "git_diff_staged":
- handler = gs.HandleGitDiffStaged
- case "git_diff":
- handler = gs.HandleGitDiff
- case "git_commit":
- handler = gs.HandleGitCommit
- case "git_add":
- handler = gs.HandleGitAdd
- case "git_reset":
- handler = gs.HandleGitReset
- case "git_log":
- handler = gs.HandleGitLog
- case "git_create_branch":
- handler = gs.HandleGitCreateBranch
- case "git_checkout":
- handler = gs.HandleGitCheckout
- case "git_show":
- handler = gs.HandleGitShow
- case "git_init":
- handler = gs.HandleGitInit
- default:
- continue
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Commit history:\n%s", output))), nil
+ }))
+
+ // Add git_create_branch tool
+ builder.AddTool(mcp.NewTool("git_create_branch", "Creates a new branch from an optional base branch", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ "branch_name": map[string]interface{}{
+ "type": "string",
+ "description": "Name of the new branch",
+ },
+ "base_branch": map[string]interface{}{
+ "type": "string",
+ "description": "Base branch to create from (optional)",
+ },
+ },
+ "required": []string{"repo_path", "branch_name"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ repoPath = git.repoPath
+ }
+
+ branchName, ok := req.Arguments["branch_name"].(string)
+ if !ok {
+ return mcp.NewToolError("branch_name is required"), nil
}
- gs.RegisterToolWithDefinition(tool, handler)
- }
-}
-// registerPrompts registers all Git prompts with the server
-func (gs *Server) registerPrompts() {
- commitPrompt := mcp.Prompt{
- Name: "commit-message",
- Description: "Prompt for crafting a well-structured git commit message",
- Arguments: []mcp.PromptArgument{
- {
- Name: "changes",
- Description: "Description of the changes being committed",
- Required: true,
+ baseBranch, _ := req.Arguments["base_branch"].(string)
+
+ var args []string
+ if baseBranch != "" {
+ args = []string{"checkout", "-b", branchName, baseBranch}
+ } else {
+ args = []string{"checkout", "-b", branchName}
+ }
+
+ _, err := git.runGitCommand(repoPath, args...)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git create branch failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Created branch '%s'", branchName))), nil
+ }))
+
+ // Add git_checkout tool
+ builder.AddTool(mcp.NewTool("git_checkout", "Switches branches", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
+ },
+ "branch_name": map[string]interface{}{
+ "type": "string",
+ "description": "Name of the branch to checkout",
+ },
+ },
+ "required": []string{"repo_path", "branch_name"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ repoPath = git.repoPath
+ }
+
+ branchName, ok := req.Arguments["branch_name"].(string)
+ if !ok {
+ return mcp.NewToolError("branch_name is required"), nil
+ }
+
+ _, err := git.runGitCommand(repoPath, "checkout", branchName)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git checkout failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Switched to branch '%s'", branchName))), nil
+ }))
+
+ // Add git_show tool
+ builder.AddTool(mcp.NewTool("git_show", "Shows the contents of a commit", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path to the Git repository",
},
- {
- Name: "type",
- Description: "Type of change (feat, fix, docs, style, refactor, test, chore)",
- Required: false,
+ "revision": map[string]interface{}{
+ "type": "string",
+ "description": "Commit hash or reference to show",
},
- {
- Name: "breaking",
- Description: "Whether this is a breaking change (true/false)",
- Required: false,
+ },
+ "required": []string{"repo_path", "revision"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ repoPath = git.repoPath
+ }
+
+ revision, ok := req.Arguments["revision"].(string)
+ if !ok {
+ return mcp.NewToolError("revision is required"), nil
+ }
+
+ output, err := git.runGitCommand(repoPath, "show", revision)
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git show failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(output)), nil
+ }))
+
+ // Add git_init tool
+ builder.AddTool(mcp.NewTool("git_init", "Initialize a new Git repository", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "repo_path": map[string]interface{}{
+ "type": "string",
+ "description": "Path where to initialize the Git repository",
},
},
+ "required": []string{"repo_path"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ repoPath, ok := req.Arguments["repo_path"].(string)
+ if !ok {
+ repoPath = git.repoPath
+ }
+
+ if err := os.MkdirAll(repoPath, 0755); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("failed to create directory: %v", err)), nil
+ }
+
+ _, err := git.runGitCommand(repoPath, "init")
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("git init failed: %v", err)), nil
+ }
+
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Initialized empty Git repository in %s", repoPath))), nil
+ }))
+
+ // Add commit-message prompt
+ builder.AddPrompt(mcp.NewPrompt("commit-message", "Prompt for crafting a well-structured git commit message", []mcp.PromptArgument{
+ {
+ Name: "changes",
+ Description: "Description of the changes being committed",
+ Required: true,
+ },
+ {
+ Name: "type",
+ Description: "Type of change (feat, fix, docs, style, refactor, test, chore)",
+ Required: false,
+ },
+ {
+ Name: "breaking",
+ Description: "Whether this is a breaking change (true/false)",
+ Required: false,
+ },
+ }, func(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
+ changes, hasChanges := req.Arguments["changes"].(string)
+ commitType, hasType := req.Arguments["type"].(string)
+ breaking, hasBreaking := req.Arguments["breaking"]
+
+ if !hasChanges || changes == "" {
+ return mcp.GetPromptResult{}, fmt.Errorf("changes argument is required")
+ }
+
+ if !hasType || commitType == "" {
+ commitType = "feat"
+ }
+
+ isBreaking := false
+ if hasBreaking {
+ if breakingBool, ok := breaking.(bool); ok {
+ isBreaking = breakingBool
+ } else if breakingStr, ok := breaking.(string); ok {
+ isBreaking = breakingStr == "true"
+ }
+ }
+
+ var messages []mcp.PromptMessage
+
+ userContent := fmt.Sprintf(`I need help writing a git commit message for the following changes:
+
+%s
+
+Please help me craft a well-structured commit message following conventional commit format.`, changes)
+
+ if hasType {
+ userContent += fmt.Sprintf("\n\nCommit type: %s", commitType)
+ }
+ if isBreaking {
+ userContent += "\n\nThis is a BREAKING CHANGE."
+ }
+
+ messages = append(messages, mcp.PromptMessage{
+ Role: "user",
+ Content: mcp.NewTextContent(userContent),
+ })
+
+ breakingPrefix := ""
+ if isBreaking {
+ breakingPrefix = "!"
+ }
+
+ assistantContent := fmt.Sprintf(`I'll help you create a conventional commit message. Here's the suggested format:
+
+**Commit message:**
+%s%s: %s
+
+**Format explanation:**
+- Type: %s (indicates the nature of the change)
+- Description: Clear, concise summary in present tense
+%s
+
+**Additional guidelines:**
+- Keep the subject line under 50 characters
+- Use imperative mood ("add" not "added")
+- Don't end subject line with a period
+- Include body if needed to explain what and why`,
+ commitType, breakingPrefix, changes,
+ commitType,
+ func() string {
+ if isBreaking {
+ return "- Breaking change: This change breaks backward compatibility"
+ }
+ return ""
+ }())
+
+ messages = append(messages, mcp.PromptMessage{
+ Role: "assistant",
+ Content: mcp.NewTextContent(assistantContent),
+ })
+
+ description := fmt.Sprintf("Commit message guidance for %s changes", commitType)
+ if isBreaking {
+ description += " (BREAKING)"
+ }
+
+ return mcp.GetPromptResult{
+ Description: description,
+ Messages: messages,
+ }, nil
+ }))
+
+ // Add git:// pattern resource for dynamic file access
+ builder.AddResource(mcp.NewResource(
+ "git://",
+ "Git Repository",
+ "",
+ func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
+ return git.handleGitResource(req)
+ },
+ ))
+
+ // Add repository root if it's a git repository
+ gitDir := filepath.Join(git.repoPath, ".git")
+ if _, err := os.Stat(gitDir); err == nil {
+ currentBranch, err := git.getCurrentBranch()
+ if err != nil {
+ currentBranch = "unknown"
+ }
+
+ gitURI := "git://" + git.repoPath
+ repoName := filepath.Base(git.repoPath)
+ if repoName == "." || repoName == "/" {
+ repoName = git.repoPath
+ }
+
+ rootName := fmt.Sprintf("Git Repository: %s (branch: %s)", repoName, currentBranch)
+ builder.AddRoot(mcp.NewRoot(gitURI, rootName))
}
- gs.RegisterPrompt(commitPrompt, gs.HandleCommitMessagePrompt)
+ return builder.Build()
}
-// registerResources sets up resource handling (lazy loading)
-func (gs *Server) registerResources() {
- // Register pattern-based git resource handlers instead of discovering all files
- // This avoids loading all repository files into memory at startup
- gs.Server.RegisterResource("git://", gs.HandleGitResource)
-}
+// Helper methods for GitOperations
-// registerRoots registers git repository as a root
-func (gs *Server) registerRoots() {
- // Check if this is a git repository
- gitDir := filepath.Join(gs.repoPath, ".git")
- if _, err := os.Stat(gitDir); os.IsNotExist(err) {
- // Not a git repository, skip root registration
- return
+func (git *GitOperations) runGitCommand(repoPath string, args ...string) (string, error) {
+ if _, err := os.Stat(repoPath); os.IsNotExist(err) {
+ return "", fmt.Errorf("repository path does not exist: %s", repoPath)
}
- // Get current branch for additional info
- currentBranch, err := gs.getCurrentBranch()
- if err != nil {
- currentBranch = "unknown"
+ if len(args) > 0 && args[0] != "init" {
+ gitDir := filepath.Join(repoPath, ".git")
+ if _, err := os.Stat(gitDir); os.IsNotExist(err) {
+ return "", fmt.Errorf("not a git repository: %s", repoPath)
+ }
}
- // Create git:// URI for the repository
- gitURI := "git://" + gs.repoPath
+ cmd := exec.Command("git", args...)
+ cmd.Dir = repoPath
- // Create a user-friendly name from the repository path
- repoName := filepath.Base(gs.repoPath)
- if repoName == "." || repoName == "/" {
- repoName = gs.repoPath
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ return "", fmt.Errorf("%v: %s", err, string(output))
}
- // Include current branch information in the name
- rootName := fmt.Sprintf("Git Repository: %s (branch: %s)", repoName, currentBranch)
-
- root := mcp.NewRoot(gitURI, rootName)
- gs.RegisterRoot(root)
+ return strings.TrimSpace(string(output)), nil
}
-
-// getCurrentBranch gets the current git branch
-func (gs *Server) getCurrentBranch() (string, error) {
- output, err := gs.runGitCommand(gs.repoPath, "branch", "--show-current")
+func (git *GitOperations) getCurrentBranch() (string, error) {
+ output, err := git.runGitCommand(git.repoPath, "branch", "--show-current")
if err != nil {
return "", err
}
branch := strings.TrimSpace(output)
if branch == "" {
- // Fallback for detached HEAD
branch = "HEAD"
}
return branch, nil
}
-// getTrackedFiles gets list of tracked files in the repository
-func (gs *Server) getTrackedFiles() ([]string, error) {
- output, err := gs.runGitCommand(gs.repoPath, "ls-files")
+func (git *GitOperations) getTrackedFiles() ([]string, error) {
+ output, err := git.runGitCommand(git.repoPath, "ls-files")
if err != nil {
return nil, err
}
@@ -278,21 +601,16 @@ func (gs *Server) getTrackedFiles() ([]string, error) {
for _, file := range files {
file = strings.TrimSpace(file)
- if file != "" {
- // Skip hidden files and certain patterns
- if !strings.HasPrefix(file, ".") {
- filteredFiles = append(filteredFiles, file)
- }
+ if file != "" && !strings.HasPrefix(file, ".") {
+ filteredFiles = append(filteredFiles, file)
}
}
return filteredFiles, nil
}
-
-// getBranches gets list of git branches
-func (gs *Server) getBranches() ([]string, error) {
- output, err := gs.runGitCommand(gs.repoPath, "branch", "--format=%(refname:short)")
+func (git *GitOperations) getBranches() ([]string, error) {
+ output, err := git.runGitCommand(git.repoPath, "branch", "--format=%(refname:short)")
if err != nil {
return nil, err
}
@@ -314,17 +632,8 @@ func (gs *Server) getBranches() ([]string, error) {
return filteredBranches, nil
}
-// Commit represents a git commit
-type Commit struct {
- Hash string
- Message string
- Author string
- Date string
-}
-
-// getRecentCommits gets recent git commits
-func (gs *Server) getRecentCommits(count int) ([]Commit, error) {
- output, err := gs.runGitCommand(gs.repoPath, "log", "--format=%H|%s|%an|%ad", "--date=short", "-n", strconv.Itoa(count))
+func (git *GitOperations) getRecentCommits(count int) ([]Commit, error) {
+ output, err := git.runGitCommand(git.repoPath, "log", "--format=%H|%s|%an|%ad", "--date=short", "-n", strconv.Itoa(count))
if err != nil {
return nil, err
}
@@ -356,14 +665,12 @@ func (gs *Server) getRecentCommits(count int) ([]Commit, error) {
return commits, nil
}
-// HandleGitResource handles git:// resource requests
-func (gs *Server) HandleGitResource(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
- // Parse git:// URI: git://repo/branch/path or git://repo/commit/hash or git://repo/branch/name
+func (git *GitOperations) handleGitResource(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
if !strings.HasPrefix(req.URI, "git://") {
return mcp.ReadResourceResult{}, fmt.Errorf("invalid git URI: %s", req.URI)
}
- uriPath := req.URI[6:] // Remove "git://" prefix
+ uriPath := req.URI[6:]
parts := strings.Split(uriPath, "/")
if len(parts) < 3 {
@@ -374,27 +681,23 @@ func (gs *Server) HandleGitResource(req mcp.ReadResourceRequest) (mcp.ReadResour
resourceType := parts[1]
resourcePath := strings.Join(parts[2:], "/")
- // Validate repository path
- if repoPath != gs.repoPath {
+ if repoPath != git.repoPath {
return mcp.ReadResourceResult{}, fmt.Errorf("access denied: repository path mismatch")
}
switch resourceType {
case "branch":
- return gs.handleBranchResource(resourcePath)
+ return git.handleBranchResource(resourcePath)
case "commit":
- return gs.handleCommitResource(resourcePath)
+ return git.handleCommitResource(resourcePath)
default:
- // Treat as file path with branch
- return gs.handleFileResource(resourceType, resourcePath)
+ return git.handleFileResource(resourceType, resourcePath)
}
}
-// handleFileResource handles git file resources
-func (gs *Server) handleFileResource(branch, filePath string) (mcp.ReadResourceResult, error) {
- // Use git show to get file content from specific branch
+func (git *GitOperations) handleFileResource(branch, filePath string) (mcp.ReadResourceResult, error) {
gitPath := fmt.Sprintf("%s:%s", branch, filePath)
- output, err := gs.runGitCommand(gs.repoPath, "show", gitPath)
+ output, err := git.runGitCommand(git.repoPath, "show", gitPath)
if err != nil {
return mcp.ReadResourceResult{}, fmt.Errorf("failed to read git file: %v", err)
}
@@ -406,10 +709,8 @@ func (gs *Server) handleFileResource(branch, filePath string) (mcp.ReadResourceR
}, nil
}
-// handleBranchResource handles git branch resources
-func (gs *Server) handleBranchResource(branchName string) (mcp.ReadResourceResult, error) {
- // Get branch information
- output, err := gs.runGitCommand(gs.repoPath, "log", "--oneline", "-n", "5", branchName)
+func (git *GitOperations) handleBranchResource(branchName string) (mcp.ReadResourceResult, error) {
+ output, err := git.runGitCommand(git.repoPath, "log", "--oneline", "-n", "5", branchName)
if err != nil {
return mcp.ReadResourceResult{}, fmt.Errorf("failed to get branch info: %v", err)
}
@@ -423,10 +724,8 @@ func (gs *Server) handleBranchResource(branchName string) (mcp.ReadResourceResul
}, nil
}
-// handleCommitResource handles git commit resources
-func (gs *Server) handleCommitResource(commitHash string) (mcp.ReadResourceResult, error) {
- // Get commit details
- output, err := gs.runGitCommand(gs.repoPath, "show", "--stat", commitHash)
+func (git *GitOperations) handleCommitResource(commitHash string) (mcp.ReadResourceResult, error) {
+ output, err := git.runGitCommand(git.repoPath, "show", "--stat", commitHash)
if err != nil {
return mcp.ReadResourceResult{}, fmt.Errorf("failed to get commit info: %v", err)
}
@@ -438,6 +737,26 @@ func (gs *Server) handleCommitResource(commitHash string) (mcp.ReadResourceResul
}, nil
}
+// Helper function to convert interface{} to []string
+func convertToStringSlice(input interface{}) ([]string, error) {
+ switch v := input.(type) {
+ case []interface{}:
+ result := make([]string, len(v))
+ for i, item := range v {
+ str, ok := item.(string)
+ if !ok {
+ return nil, fmt.Errorf("item at index %d is not a string", i)
+ }
+ result[i] = str
+ }
+ return result, nil
+ case []string:
+ return v, nil
+ default:
+ return nil, fmt.Errorf("input is not a slice")
+ }
+}
+
// Helper function to determine MIME type for git files
func getGitMimeType(filePath string) string {
ext := strings.ToLower(filepath.Ext(filePath))
@@ -468,582 +787,3 @@ func getGitMimeType(filePath string) string {
return "text/plain"
}
}
-
-// ListTools returns all available Git tools
-func (gs *Server) ListTools() []mcp.Tool {
- return []mcp.Tool{
- {
- Name: "git_status",
- Description: "Shows the working tree status",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "repo_path": map[string]interface{}{
- "type": "string",
- "description": "Path to the Git repository",
- },
- },
- "required": []string{"repo_path"},
- },
- },
- {
- Name: "git_diff_unstaged",
- Description: "Shows changes in the working directory that are not yet staged",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "repo_path": map[string]interface{}{
- "type": "string",
- "description": "Path to the Git repository",
- },
- },
- "required": []string{"repo_path"},
- },
- },
- {
- Name: "git_diff_staged",
- Description: "Shows changes that are staged for commit",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "repo_path": map[string]interface{}{
- "type": "string",
- "description": "Path to the Git repository",
- },
- },
- "required": []string{"repo_path"},
- },
- },
- {
- Name: "git_diff",
- Description: "Shows differences between branches or commits",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "repo_path": map[string]interface{}{
- "type": "string",
- "description": "Path to the Git repository",
- },
- "target": map[string]interface{}{
- "type": "string",
- "description": "Target branch or commit to diff against",
- },
- },
- "required": []string{"repo_path", "target"},
- },
- },
- {
- Name: "git_commit",
- Description: "Records changes to the repository",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "repo_path": map[string]interface{}{
- "type": "string",
- "description": "Path to the Git repository",
- },
- "message": map[string]interface{}{
- "type": "string",
- "description": "Commit message",
- },
- },
- "required": []string{"repo_path", "message"},
- },
- },
- {
- Name: "git_add",
- Description: "Adds file contents to the staging area",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "repo_path": map[string]interface{}{
- "type": "string",
- "description": "Path to the Git repository",
- },
- "files": map[string]interface{}{
- "type": "array",
- "items": map[string]interface{}{
- "type": "string",
- },
- "description": "List of files to add",
- },
- },
- "required": []string{"repo_path", "files"},
- },
- },
- {
- Name: "git_reset",
- Description: "Unstages all staged changes",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "repo_path": map[string]interface{}{
- "type": "string",
- "description": "Path to the Git repository",
- },
- },
- "required": []string{"repo_path"},
- },
- },
- {
- Name: "git_log",
- Description: "Shows the commit logs",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "repo_path": map[string]interface{}{
- "type": "string",
- "description": "Path to the Git repository",
- },
- "max_count": map[string]interface{}{
- "type": "integer",
- "description": "Maximum number of commits to show",
- "default": 10,
- },
- },
- "required": []string{"repo_path"},
- },
- },
- {
- Name: "git_create_branch",
- Description: "Creates a new branch from an optional base branch",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "repo_path": map[string]interface{}{
- "type": "string",
- "description": "Path to the Git repository",
- },
- "branch_name": map[string]interface{}{
- "type": "string",
- "description": "Name of the new branch",
- },
- "base_branch": map[string]interface{}{
- "type": "string",
- "description": "Base branch to create from (optional)",
- },
- },
- "required": []string{"repo_path", "branch_name"},
- },
- },
- {
- Name: "git_checkout",
- Description: "Switches branches",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "repo_path": map[string]interface{}{
- "type": "string",
- "description": "Path to the Git repository",
- },
- "branch_name": map[string]interface{}{
- "type": "string",
- "description": "Name of the branch to checkout",
- },
- },
- "required": []string{"repo_path", "branch_name"},
- },
- },
- {
- Name: "git_show",
- Description: "Shows the contents of a commit",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "repo_path": map[string]interface{}{
- "type": "string",
- "description": "Path to the Git repository",
- },
- "revision": map[string]interface{}{
- "type": "string",
- "description": "Commit hash or reference to show",
- },
- },
- "required": []string{"repo_path", "revision"},
- },
- },
- {
- Name: "git_init",
- Description: "Initialize a new Git repository",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "repo_path": map[string]interface{}{
- "type": "string",
- "description": "Path where to initialize the Git repository",
- },
- },
- "required": []string{"repo_path"},
- },
- },
- }
-}
-
-// Tool handlers
-
-func (gs *Server) HandleGitStatus(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- repoPath, ok := req.Arguments["repo_path"].(string)
- if !ok {
- repoPath = gs.repoPath
- }
-
- output, err := gs.runGitCommand(repoPath, "status")
- if err != nil {
- return mcp.NewToolError(fmt.Sprintf("git status failed: %v", err)), nil
- }
-
- return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Repository status:\n%s", output))), nil
-}
-
-func (gs *Server) HandleGitDiffUnstaged(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- repoPath, ok := req.Arguments["repo_path"].(string)
- if !ok {
- repoPath = gs.repoPath
- }
-
- output, err := gs.runGitCommand(repoPath, "diff")
- if err != nil {
- return mcp.NewToolError(fmt.Sprintf("git diff failed: %v", err)), nil
- }
-
- return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Unstaged changes:\n%s", output))), nil
-}
-
-func (gs *Server) HandleGitDiffStaged(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- repoPath, ok := req.Arguments["repo_path"].(string)
- if !ok {
- repoPath = gs.repoPath
- }
-
- output, err := gs.runGitCommand(repoPath, "diff", "--cached")
- if err != nil {
- return mcp.NewToolError(fmt.Sprintf("git diff --cached failed: %v", err)), nil
- }
-
- return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Staged changes:\n%s", output))), nil
-}
-
-func (gs *Server) HandleGitDiff(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- repoPath, ok := req.Arguments["repo_path"].(string)
- if !ok {
- repoPath = gs.repoPath
- }
-
- target, ok := req.Arguments["target"].(string)
- if !ok {
- return mcp.NewToolError("target is required"), nil
- }
-
- output, err := gs.runGitCommand(repoPath, "diff", target)
- if err != nil {
- return mcp.NewToolError(fmt.Sprintf("git diff failed: %v", err)), nil
- }
-
- return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Diff with %s:\n%s", target, output))), nil
-}
-
-func (gs *Server) HandleGitCommit(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- repoPath, ok := req.Arguments["repo_path"].(string)
- if !ok {
- repoPath = gs.repoPath
- }
-
- message, ok := req.Arguments["message"].(string)
- if !ok {
- return mcp.NewToolError("message is required"), nil
- }
-
- output, err := gs.runGitCommand(repoPath, "commit", "-m", message)
- if err != nil {
- return mcp.NewToolError(fmt.Sprintf("git commit failed: %v", err)), nil
- }
-
- return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Changes committed successfully:\n%s", output))), nil
-}
-
-func (gs *Server) HandleGitAdd(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- repoPath, ok := req.Arguments["repo_path"].(string)
- if !ok {
- repoPath = gs.repoPath
- }
-
- filesInterface, ok := req.Arguments["files"]
- if !ok {
- return mcp.NewToolError("files is required"), nil
- }
-
- files, err := gs.convertToStringSlice(filesInterface)
- if err != nil {
- return mcp.NewToolError("files must be an array of strings"), nil
- }
-
- args := append([]string{"add"}, files...)
- _, err = gs.runGitCommand(repoPath, args...)
- if err != nil {
- return mcp.NewToolError(fmt.Sprintf("git add failed: %v", err)), nil
- }
-
- return mcp.NewToolResult(mcp.NewTextContent("Files staged successfully")), nil
-}
-
-func (gs *Server) HandleGitReset(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- repoPath, ok := req.Arguments["repo_path"].(string)
- if !ok {
- repoPath = gs.repoPath
- }
-
- _, err := gs.runGitCommand(repoPath, "reset")
- if err != nil {
- return mcp.NewToolError(fmt.Sprintf("git reset failed: %v", err)), nil
- }
-
- return mcp.NewToolResult(mcp.NewTextContent("All staged changes reset")), nil
-}
-
-func (gs *Server) HandleGitLog(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- repoPath, ok := req.Arguments["repo_path"].(string)
- if !ok {
- repoPath = gs.repoPath
- }
-
- maxCount := 10
- if mc, exists := req.Arguments["max_count"]; exists {
- if count, ok := mc.(float64); ok {
- maxCount = int(count)
- }
- }
-
- output, err := gs.runGitCommand(repoPath, "log", "--oneline", "-n", strconv.Itoa(maxCount))
- if err != nil {
- return mcp.NewToolError(fmt.Sprintf("git log failed: %v", err)), nil
- }
-
- return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Commit history:\n%s", output))), nil
-}
-
-func (gs *Server) HandleGitCreateBranch(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- repoPath, ok := req.Arguments["repo_path"].(string)
- if !ok {
- repoPath = gs.repoPath
- }
-
- branchName, ok := req.Arguments["branch_name"].(string)
- if !ok {
- return mcp.NewToolError("branch_name is required"), nil
- }
-
- baseBranch, _ := req.Arguments["base_branch"].(string)
-
- var args []string
- if baseBranch != "" {
- args = []string{"checkout", "-b", branchName, baseBranch}
- } else {
- args = []string{"checkout", "-b", branchName}
- }
-
- _, err := gs.runGitCommand(repoPath, args...)
- if err != nil {
- return mcp.NewToolError(fmt.Sprintf("git create branch failed: %v", err)), nil
- }
-
- return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Created branch '%s'", branchName))), nil
-}
-
-func (gs *Server) HandleGitCheckout(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- repoPath, ok := req.Arguments["repo_path"].(string)
- if !ok {
- repoPath = gs.repoPath
- }
-
- branchName, ok := req.Arguments["branch_name"].(string)
- if !ok {
- return mcp.NewToolError("branch_name is required"), nil
- }
-
- _, err := gs.runGitCommand(repoPath, "checkout", branchName)
- if err != nil {
- return mcp.NewToolError(fmt.Sprintf("git checkout failed: %v", err)), nil
- }
-
- return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Switched to branch '%s'", branchName))), nil
-}
-
-func (gs *Server) HandleGitShow(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- repoPath, ok := req.Arguments["repo_path"].(string)
- if !ok {
- repoPath = gs.repoPath
- }
-
- revision, ok := req.Arguments["revision"].(string)
- if !ok {
- return mcp.NewToolError("revision is required"), nil
- }
-
- output, err := gs.runGitCommand(repoPath, "show", revision)
- if err != nil {
- return mcp.NewToolError(fmt.Sprintf("git show failed: %v", err)), nil
- }
-
- return mcp.NewToolResult(mcp.NewTextContent(output)), nil
-}
-
-func (gs *Server) HandleGitInit(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- repoPath, ok := req.Arguments["repo_path"].(string)
- if !ok {
- repoPath = gs.repoPath
- }
-
- // Ensure directory exists
- if err := os.MkdirAll(repoPath, 0755); err != nil {
- return mcp.NewToolError(fmt.Sprintf("failed to create directory: %v", err)), nil
- }
-
- _, err := gs.runGitCommand(repoPath, "init")
- if err != nil {
- return mcp.NewToolError(fmt.Sprintf("git init failed: %v", err)), nil
- }
-
- return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Initialized empty Git repository in %s", repoPath))), nil
-}
-
-// Helper methods
-
-func (gs *Server) runGitCommand(repoPath string, args ...string) (string, error) {
- // Check if path exists
- if _, err := os.Stat(repoPath); os.IsNotExist(err) {
- return "", fmt.Errorf("repository path does not exist: %s", repoPath)
- }
-
- // Check if it's a git repository (except for init command)
- if len(args) > 0 && args[0] != "init" {
- gitDir := filepath.Join(repoPath, ".git")
- if _, err := os.Stat(gitDir); os.IsNotExist(err) {
- return "", fmt.Errorf("not a git repository: %s", repoPath)
- }
- }
-
- cmd := exec.Command("git", args...)
- cmd.Dir = repoPath
-
- output, err := cmd.CombinedOutput()
- if err != nil {
- return "", fmt.Errorf("%v: %s", err, string(output))
- }
-
- return strings.TrimSpace(string(output)), nil
-}
-
-// Prompt handlers
-
-func (gs *Server) HandleCommitMessagePrompt(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
- changes, hasChanges := req.Arguments["changes"].(string)
- commitType, hasType := req.Arguments["type"].(string)
- breaking, hasBreaking := req.Arguments["breaking"]
-
- if !hasChanges || changes == "" {
- return mcp.GetPromptResult{}, fmt.Errorf("changes argument is required")
- }
-
- // Default type if not provided
- if !hasType || commitType == "" {
- commitType = "feat" // Default to feature
- }
-
- // Parse breaking change flag
- isBreaking := false
- if hasBreaking {
- if breakingBool, ok := breaking.(bool); ok {
- isBreaking = breakingBool
- } else if breakingStr, ok := breaking.(string); ok {
- isBreaking = breakingStr == "true"
- }
- }
-
- // Create the prompt messages
- var messages []mcp.PromptMessage
-
- // User message describing the changes
- userContent := fmt.Sprintf(`I need help writing a git commit message for the following changes:
-
-%s
-
-Please help me craft a well-structured commit message following conventional commit format.`, changes)
-
- if hasType {
- userContent += fmt.Sprintf("\n\nCommit type: %s", commitType)
- }
- if isBreaking {
- userContent += "\n\nThis is a BREAKING CHANGE."
- }
-
- messages = append(messages, mcp.PromptMessage{
- Role: "user",
- Content: mcp.NewTextContent(userContent),
- })
-
- // Assistant message with suggested commit format
- breakingPrefix := ""
- if isBreaking {
- breakingPrefix = "!"
- }
-
- assistantContent := fmt.Sprintf(`I'll help you create a conventional commit message. Here's the suggested format:
-
-**Commit message:**
-%s%s: %s
-
-**Format explanation:**
-- Type: %s (indicates the nature of the change)
-- Description: Clear, concise summary in present tense
-%s
-
-**Additional guidelines:**
-- Keep the subject line under 50 characters
-- Use imperative mood ("add" not "added")
-- Don't end subject line with a period
-- Include body if needed to explain what and why`,
- commitType, breakingPrefix, changes,
- commitType,
- func() string {
- if isBreaking {
- return "- Breaking change: This change breaks backward compatibility"
- }
- return ""
- }())
-
- messages = append(messages, mcp.PromptMessage{
- Role: "assistant",
- Content: mcp.NewTextContent(assistantContent),
- })
-
- description := fmt.Sprintf("Commit message guidance for %s changes", commitType)
- if isBreaking {
- description += " (BREAKING)"
- }
-
- return mcp.GetPromptResult{
- Description: description,
- Messages: messages,
- }, nil
-}
-
-// Helper methods
-
-func (gs *Server) convertToStringSlice(input interface{}) ([]string, error) {
- switch v := input.(type) {
- case []interface{}:
- result := make([]string, len(v))
- for i, item := range v {
- str, ok := item.(string)
- if !ok {
- return nil, fmt.Errorf("item at index %d is not a string", i)
- }
- result[i] = str
- }
- return result, nil
- case []string:
- return v, nil
- default:
- return nil, fmt.Errorf("input is not a slice")
- }
-}
pkg/mcp/server.go
@@ -8,49 +8,148 @@ import (
"log"
"os"
"strings"
- "sync"
)
-// Server represents an MCP server
+// ServerBuilder helps build immutable MCP servers with a fluent API
+type ServerBuilder struct {
+ name string
+ version string
+ tools []Tool
+ prompts []Prompt
+ resources []Resource
+ roots []Root
+
+ // Optional handlers
+ initializeHandler func(InitializeRequest) (InitializeResult, error)
+ shutdownHandler func() error
+}
+
+// Server represents an immutable MCP server
type Server struct {
name string
version string
capabilities ServerCapabilities
- // Handler functions
+ // Immutable component definitions (handlers embedded in structs)
toolDefinitions map[string]Tool
- promptHandlers map[string]PromptHandler
promptDefinitions map[string]Prompt
- resourceHandlers map[string]ResourceHandler
resourceDefinitions map[string]Resource
rootDefinitions map[string]Root
// Lifecycle handlers
initializeHandler func(InitializeRequest) (InitializeResult, error)
shutdownHandler func() error
-
- // Custom request handlers for overriding default behavior
- customRequestHandlers map[string]func(JSONRPCRequest) JSONRPCResponse
-
- mu sync.RWMutex
}
// Handler types
-type PromptHandler func(GetPromptRequest) (GetPromptResult, error)
type ResourceHandler func(ReadResourceRequest) (ReadResourceResult, error)
-// NewServer creates a new MCP server
+// NewServerBuilder creates a new server builder
+func NewServerBuilder(name, version string) *ServerBuilder {
+ return &ServerBuilder{
+ name: name,
+ version: version,
+ tools: []Tool{},
+ prompts: []Prompt{},
+ resources: []Resource{},
+ roots: []Root{},
+ }
+}
+
+// AddTool adds a tool to the server
+func (b *ServerBuilder) AddTool(tool Tool) *ServerBuilder {
+ b.tools = append(b.tools, tool)
+ return b
+}
+
+// AddPrompt adds a prompt to the server
+func (b *ServerBuilder) AddPrompt(prompt Prompt) *ServerBuilder {
+ b.prompts = append(b.prompts, prompt)
+ return b
+}
+
+// AddResource adds a resource to the server
+func (b *ServerBuilder) AddResource(resource Resource) *ServerBuilder {
+ b.resources = append(b.resources, resource)
+ return b
+}
+
+// AddRoot adds a root to the server
+func (b *ServerBuilder) AddRoot(root Root) *ServerBuilder {
+ b.roots = append(b.roots, root)
+ return b
+}
+
+// SetInitializeHandler sets the initialize handler
+func (b *ServerBuilder) SetInitializeHandler(handler func(InitializeRequest) (InitializeResult, error)) *ServerBuilder {
+ b.initializeHandler = handler
+ return b
+}
+
+// SetShutdownHandler sets the shutdown handler
+func (b *ServerBuilder) SetShutdownHandler(handler func() error) *ServerBuilder {
+ b.shutdownHandler = handler
+ return b
+}
+
+// Build creates an immutable server with dynamic capabilities
+func (b *ServerBuilder) Build() *Server {
+ // Build dynamic capabilities based on what components are present
+ capabilities := ServerCapabilities{
+ Logging: &LoggingCapability{}, // Always present
+ }
+
+ if len(b.tools) > 0 {
+ capabilities.Tools = &ToolsCapability{}
+ }
+ if len(b.prompts) > 0 {
+ capabilities.Prompts = &PromptsCapability{}
+ }
+ if len(b.resources) > 0 {
+ capabilities.Resources = &ResourcesCapability{}
+ }
+ if len(b.roots) > 0 {
+ capabilities.Roots = &RootsCapability{}
+ }
+
+ server := &Server{
+ name: b.name,
+ version: b.version,
+ capabilities: capabilities,
+ toolDefinitions: make(map[string]Tool),
+ promptDefinitions: make(map[string]Prompt),
+ resourceDefinitions: make(map[string]Resource),
+ rootDefinitions: make(map[string]Root),
+ initializeHandler: b.initializeHandler,
+ shutdownHandler: b.shutdownHandler,
+ }
+
+ // Register all components
+ for _, tool := range b.tools {
+ server.toolDefinitions[tool.Name] = tool
+ }
+ for _, prompt := range b.prompts {
+ server.promptDefinitions[prompt.Name] = prompt
+ }
+ for _, resource := range b.resources {
+ server.resourceDefinitions[resource.URI] = resource
+ }
+ for _, root := range b.roots {
+ server.rootDefinitions[root.URI] = root
+ }
+
+ return server
+}
+
+// NewServer creates a new MCP server (deprecated - use ServerBuilder instead)
func NewServer(name, version string, tools []Tool, resources []Resource, roots []Root) *Server {
server := &Server{
- name: name,
- version: version,
- toolDefinitions: make(map[string]Tool),
- promptHandlers: make(map[string]PromptHandler),
- promptDefinitions: make(map[string]Prompt),
- resourceHandlers: make(map[string]ResourceHandler),
- resourceDefinitions: make(map[string]Resource),
- rootDefinitions: make(map[string]Root),
- customRequestHandlers: make(map[string]func(JSONRPCRequest) JSONRPCResponse),
+ name: name,
+ version: version,
+ toolDefinitions: make(map[string]Tool),
+ promptDefinitions: make(map[string]Prompt),
+ resourceDefinitions: make(map[string]Resource),
+ rootDefinitions: make(map[string]Root),
capabilities: ServerCapabilities{
Tools: &ToolsCapability{},
Prompts: &PromptsCapability{},
@@ -65,7 +164,6 @@ func NewServer(name, version string, tools []Tool, resources []Resource, roots [
}
for _, resource := range resources {
- server.resourceHandlers[resource.URI] = resource.Handler
server.resourceDefinitions[resource.URI] = resource
}
@@ -76,43 +174,49 @@ func NewServer(name, version string, tools []Tool, resources []Resource, roots [
return server
}
+// RegisterRoot registers a root with the server (immutable servers should use ServerBuilder)
+func (s *Server) RegisterRoot(root Root) {
+ s.rootDefinitions[root.URI] = root
+}
+
+// Compatibility methods for existing servers (deprecated - use ServerBuilder instead)
+
// RegisterToolWithDefinition registers a tool with its full definition and handler
func (s *Server) RegisterToolWithDefinition(tool Tool, handler ToolHandler) {
- s.mu.Lock()
- defer s.mu.Unlock()
+ tool.Handler = handler
s.toolDefinitions[tool.Name] = tool
}
// RegisterPrompt registers a prompt with its definition and handler
func (s *Server) RegisterPrompt(prompt Prompt, handler PromptHandler) {
- s.mu.Lock()
- defer s.mu.Unlock()
- s.promptHandlers[prompt.Name] = handler
+ prompt.Handler = handler
s.promptDefinitions[prompt.Name] = prompt
}
// RegisterResource registers a resource handler with minimal definition
func (s *Server) RegisterResource(uri string, handler ResourceHandler) {
+ name := extractResourceName(uri)
+ if name == "" {
+ name = uri // Use the full URI as name if extraction fails
+ }
resource := Resource{
- URI: uri,
- Name: extractResourceName(uri),
+ URI: uri,
+ Name: name,
+ Handler: handler,
}
s.RegisterResourceWithDefinition(resource, handler)
}
// RegisterResourceWithDefinition registers a resource with its full definition and handler
func (s *Server) RegisterResourceWithDefinition(resource Resource, handler ResourceHandler) {
- s.mu.Lock()
- defer s.mu.Unlock()
- s.resourceHandlers[resource.URI] = handler
+ resource.Handler = handler
s.resourceDefinitions[resource.URI] = resource
}
-// RegisterRoot registers a root with the server
-func (s *Server) RegisterRoot(root Root) {
- s.mu.Lock()
- defer s.mu.Unlock()
- s.rootDefinitions[root.URI] = root
+// SetCustomRequestHandler sets custom request handlers for overriding default behavior
+func (s *Server) SetCustomRequestHandler(handlers map[string]func(JSONRPCRequest) JSONRPCResponse) {
+ // For now, just log that this is deprecated - we removed custom handlers from immutable architecture
+ // This is a compatibility shim
}
// SetInitializeHandler sets the initialize handler
@@ -125,64 +229,40 @@ func (s *Server) SetShutdownHandler(handler func() error) {
s.shutdownHandler = handler
}
-// SetCustomRequestHandler sets custom request handlers for overriding default behavior
-func (s *Server) SetCustomRequestHandler(handlers map[string]func(JSONRPCRequest) JSONRPCResponse) {
- s.mu.Lock()
- defer s.mu.Unlock()
- for method, handler := range handlers {
- s.customRequestHandlers[method] = handler
- }
-}
// ListTools returns all registered tools
func (s *Server) ListTools() []Tool {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
tools := make([]Tool, 0, len(s.toolDefinitions))
for _, tool := range s.toolDefinitions {
tools = append(tools, tool)
}
-
return tools
}
// ListPrompts returns all registered prompts
func (s *Server) ListPrompts() []Prompt {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
prompts := make([]Prompt, 0, len(s.promptDefinitions))
for _, prompt := range s.promptDefinitions {
prompts = append(prompts, prompt)
}
-
return prompts
}
// ListResources returns all registered resources
func (s *Server) ListResources() []Resource {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
resources := make([]Resource, 0, len(s.resourceDefinitions))
for _, resource := range s.resourceDefinitions {
resources = append(resources, resource)
}
-
return resources
}
// ListRoots returns all registered roots
func (s *Server) ListRoots() []Root {
- s.mu.RLock()
- defer s.mu.RUnlock()
-
roots := make([]Root, 0, len(s.rootDefinitions))
for _, root := range s.rootDefinitions {
roots = append(roots, root)
}
-
return roots
}
@@ -220,15 +300,6 @@ func (s *Server) Run(ctx context.Context) error {
// handleRequest processes a JSON-RPC request
func (s *Server) handleRequest(req JSONRPCRequest) JSONRPCResponse {
- // Check for custom handlers first
- s.mu.RLock()
- if customHandler, exists := s.customRequestHandlers[req.Method]; exists {
- s.mu.RUnlock()
- return customHandler(req)
- }
- s.mu.RUnlock()
-
- // Default handlers
switch req.Method {
case "initialize":
return s.handleInitialize(req)
@@ -307,10 +378,7 @@ func (s *Server) handleCallTool(req JSONRPCRequest) JSONRPCResponse {
return s.createErrorResponse(req.ID, InvalidParams, "Invalid params")
}
- s.mu.RLock()
tool, exists := s.toolDefinitions[callReq.Name]
- s.mu.RUnlock()
-
if !exists {
return s.createErrorResponse(req.ID, MethodNotFound, "Tool not found")
}
@@ -335,15 +403,12 @@ func (s *Server) handleGetPrompt(req JSONRPCRequest) JSONRPCResponse {
return s.createErrorResponse(req.ID, InvalidParams, "Invalid params")
}
- s.mu.RLock()
- handler, exists := s.promptHandlers[promptReq.Name]
- s.mu.RUnlock()
-
+ prompt, exists := s.promptDefinitions[promptReq.Name]
if !exists {
return s.createErrorResponse(req.ID, MethodNotFound, "Prompt not found")
}
- result, err := handler(promptReq)
+ result, err := prompt.Handler(promptReq)
if err != nil {
return s.createErrorResponse(req.ID, InternalError, err.Error())
}
@@ -369,27 +434,25 @@ func (s *Server) handleReadResource(req JSONRPCRequest) JSONRPCResponse {
return s.createErrorResponse(req.ID, InvalidParams, "Invalid params")
}
- s.mu.RLock()
- handler, exists := s.resourceHandlers[readReq.URI]
+ resource, exists := s.resourceDefinitions[readReq.URI]
if !exists {
// Try to find a pattern-based handler (e.g., for "file://" prefix)
- for pattern, h := range s.resourceHandlers {
+ for pattern, r := range s.resourceDefinitions {
if pattern != "" && readReq.URI != pattern &&
((pattern == "file://" && strings.HasPrefix(readReq.URI, "file://")) ||
(strings.HasSuffix(pattern, "*") && strings.HasPrefix(readReq.URI, strings.TrimSuffix(pattern, "*")))) {
- handler = h
+ resource = r
exists = true
break
}
}
}
- s.mu.RUnlock()
if !exists {
return s.createErrorResponse(req.ID, MethodNotFound, "Resource not found")
}
- result, err := handler(readReq)
+ result, err := resource.Handler(readReq)
if err != nil {
return s.createErrorResponse(req.ID, InternalError, err.Error())
}
@@ -471,6 +534,16 @@ func NewResource(uri, name, mimeType string, handler ResourceHandler) Resource {
}
}
+// Helper function to create a new prompt with all fields
+func NewPrompt(name, description string, arguments []PromptArgument, handler PromptHandler) Prompt {
+ return Prompt{
+ Name: name,
+ Description: description,
+ Arguments: arguments,
+ Handler: handler,
+ }
+}
+
// Helper function to extract resource name from URI
func extractResourceName(uri string) string {
// Find the last "/" in the URI and extract the part after it
pkg/mcp/types.go
@@ -138,11 +138,15 @@ func (i ImageContent) GetType() string {
return i.Type
}
+// Handler types
+type PromptHandler func(GetPromptRequest) (GetPromptResult, error)
+
// Prompt types
type Prompt struct {
Name string `json:"name"`
Description string `json:"description"`
Arguments []PromptArgument `json:"arguments,omitempty"`
+ Handler PromptHandler `json:"-"`
}
type PromptArgument struct {
pkg/memory/server.go
@@ -10,15 +10,6 @@ import (
"github.com/xlgmokha/mcp/pkg/mcp"
)
-// Server implements the Memory MCP server with knowledge graph functionality
-type Server struct {
- *mcp.Server
- memoryFile string
- graph *KnowledgeGraph
- mu sync.RWMutex
- loaded bool
-}
-
// KnowledgeGraph represents the in-memory knowledge graph
type KnowledgeGraph struct {
Entities map[string]*Entity `json:"entities"`
@@ -39,1003 +30,828 @@ type Relation struct {
RelationType string `json:"relationType"`
}
-// New creates a new Memory MCP server
-func New(memoryFile string) *Server {
- server := mcp.NewServer("mcp-memory", "1.0.0", []mcp.Tool{}, []mcp.Resource{}, []mcp.Root{})
+// MemoryOperations provides memory graph operations
+type MemoryOperations struct {
+ memoryFile string
+ graph *KnowledgeGraph
+ mu sync.RWMutex
+ loaded bool
+}
- memoryServer := &Server{
- Server: server,
+// NewMemoryOperations creates a new MemoryOperations helper
+func NewMemoryOperations(memoryFile string) *MemoryOperations {
+ return &MemoryOperations{
memoryFile: memoryFile,
graph: &KnowledgeGraph{
Entities: make(map[string]*Entity),
Relations: make(map[string]Relation),
},
}
-
- // Register all memory tools, prompts, resources, and roots
- memoryServer.registerTools()
- memoryServer.registerPrompts()
- memoryServer.registerResources()
- memoryServer.registerRoots()
-
- return memoryServer
}
-
-// ensureGraphLoaded loads the knowledge graph only when needed
-func (ms *Server) ensureGraphLoaded() error {
- ms.mu.Lock()
- defer ms.mu.Unlock()
-
- // Check if graph is already loaded
- if !ms.loaded {
- ms.loaded = true
-
- // Load from file if it exists
- return ms.loadGraphInternal()
- }
- return nil
-}
-
-
-// registerTools registers all Memory tools with the server
-func (ms *Server) registerTools() {
- // Get all tool definitions from ListTools method
- tools := ms.ListTools()
-
- // Register each tool with its proper definition
- for _, tool := range tools {
- var handler mcp.ToolHandler
- switch tool.Name {
- case "create_entities":
- handler = ms.HandleCreateEntities
- case "create_relations":
- handler = ms.HandleCreateRelations
- case "add_observations":
- handler = ms.HandleAddObservations
- case "delete_entities":
- handler = ms.HandleDeleteEntities
- case "delete_observations":
- handler = ms.HandleDeleteObservations
- case "delete_relations":
- handler = ms.HandleDeleteRelations
- case "read_graph":
- handler = ms.HandleReadGraph
- case "search_nodes":
- handler = ms.HandleSearchNodes
- case "open_nodes":
- handler = ms.HandleOpenNodes
- default:
- continue
- }
- ms.RegisterToolWithDefinition(tool, handler)
- }
-}
-
-// registerPrompts registers all Memory prompts with the server
-func (ms *Server) registerPrompts() {
- knowledgePrompt := mcp.Prompt{
- Name: "knowledge-query",
- Description: "Prompt for querying and exploring the knowledge graph",
- Arguments: []mcp.PromptArgument{
- {
- Name: "query",
- Description: "What you want to search for or ask about in the knowledge graph",
- Required: true,
- },
- {
- Name: "context",
- Description: "Additional context about your question (optional)",
- Required: false,
- },
- },
- }
-
- ms.RegisterPrompt(knowledgePrompt, ms.HandleKnowledgeQueryPrompt)
-}
-
-// registerResources sets up resource handling (lazy loading)
-func (ms *Server) registerResources() {
- // Register a placeholder memory resource to make it discoverable
- memoryURI := "memory://graph"
- resource := mcp.Resource{
- URI: memoryURI,
- Name: "Knowledge Graph",
- Description: "In-memory knowledge graph with entities and relations",
- MimeType: "application/json",
- }
-
- ms.Server.RegisterResourceWithDefinition(resource, ms.HandleMemoryResource)
-}
-
-// registerRoots registers memory knowledge graph as a root (without live statistics)
-func (ms *Server) registerRoots() {
- // Create memory:// URI for the knowledge graph
- memoryURI := "memory://graph"
-
- // Create a simple name without requiring graph loading
- rootName := "Knowledge Graph"
-
- root := mcp.NewRoot(memoryURI, rootName)
- ms.RegisterRoot(root)
-}
-
-// ListTools returns all available Memory tools
-func (ms *Server) ListTools() []mcp.Tool {
- return []mcp.Tool{
- {
- Name: "create_entities",
- Description: "Create multiple new entities in the knowledge graph",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "entities": map[string]interface{}{
- "type": "array",
- "items": map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "name": map[string]interface{}{
- "type": "string",
- "description": "The name of the entity",
- },
- "entityType": map[string]interface{}{
- "type": "string",
- "description": "The type of the entity",
- },
- "observations": map[string]interface{}{
- "type": "array",
- "items": map[string]interface{}{
- "type": "string",
- },
- "description": "An array of observation contents associated with the entity",
- },
- },
- "required": []string{"name", "entityType", "observations"},
- },
- },
- },
- "required": []string{"entities"},
- },
- },
- {
- Name: "create_relations",
- Description: "Create multiple new relations between entities in the knowledge graph. Relations should be in active voice",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "relations": map[string]interface{}{
- "type": "array",
- "items": map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "from": map[string]interface{}{
- "type": "string",
- "description": "The name of the entity where the relation starts",
- },
- "to": map[string]interface{}{
- "type": "string",
- "description": "The name of the entity where the relation ends",
- },
- "relationType": map[string]interface{}{
- "type": "string",
- "description": "The type of the relation",
- },
- },
- "required": []string{"from", "to", "relationType"},
- },
- },
- },
- "required": []string{"relations"},
- },
- },
- {
- Name: "add_observations",
- Description: "Add new observations to existing entities in the knowledge graph",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "observations": map[string]interface{}{
- "type": "array",
- "items": map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "entityName": map[string]interface{}{
- "type": "string",
- "description": "The name of the entity to add the observations to",
- },
- "contents": map[string]interface{}{
- "type": "array",
- "items": map[string]interface{}{
- "type": "string",
- },
- "description": "An array of observation contents to add",
- },
- },
- "required": []string{"entityName", "contents"},
- },
- },
- },
- "required": []string{"observations"},
- },
- },
- {
- Name: "delete_entities",
- Description: "Delete multiple entities and their associated relations from the knowledge graph",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "entityNames": map[string]interface{}{
- "type": "array",
- "items": map[string]interface{}{
- "type": "string",
+// New creates a new Memory MCP server
+func New(memoryFile string) *mcp.Server {
+ memory := NewMemoryOperations(memoryFile)
+ builder := mcp.NewServerBuilder("mcp-memory", "1.0.0")
+
+ // Add create_entities tool
+ builder.AddTool(mcp.NewTool("create_entities", "Create multiple new entities in the knowledge graph", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "entities": map[string]interface{}{
+ "type": "array",
+ "items": map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "name": map[string]interface{}{
+ "type": "string",
+ "description": "The name of the entity",
},
- "description": "An array of entity names to delete",
- },
- },
- "required": []string{"entityNames"},
- },
- },
- {
- Name: "delete_observations",
- Description: "Delete specific observations from entities in the knowledge graph",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "deletions": map[string]interface{}{
- "type": "array",
- "items": map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "entityName": map[string]interface{}{
- "type": "string",
- "description": "The name of the entity containing the observations",
- },
- "observations": map[string]interface{}{
- "type": "array",
- "items": map[string]interface{}{
- "type": "string",
- },
- "description": "An array of observations to delete",
- },
- },
- "required": []string{"entityName", "observations"},
+ "entityType": map[string]interface{}{
+ "type": "string",
+ "description": "The type of the entity",
},
- },
- },
- "required": []string{"deletions"},
- },
- },
- {
- Name: "delete_relations",
- Description: "Delete multiple relations from the knowledge graph",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "relations": map[string]interface{}{
- "type": "array",
- "items": map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "from": map[string]interface{}{
- "type": "string",
- "description": "The name of the entity where the relation starts",
- },
- "to": map[string]interface{}{
- "type": "string",
- "description": "The name of the entity where the relation ends",
- },
- "relationType": map[string]interface{}{
- "type": "string",
- "description": "The type of the relation",
- },
+ "observations": map[string]interface{}{
+ "type": "array",
+ "items": map[string]interface{}{
+ "type": "string",
},
- "required": []string{"from", "to", "relationType"},
- },
- },
- },
- "required": []string{"relations"},
- },
- },
- {
- Name: "read_graph",
- Description: "Read the entire knowledge graph",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{},
- },
- },
- {
- Name: "search_nodes",
- Description: "Search for nodes in the knowledge graph based on a query",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "query": map[string]interface{}{
- "type": "string",
- "description": "The search query to match against entity names, types, and observation content",
- },
- },
- "required": []string{"query"},
- },
- },
- {
- Name: "open_nodes",
- Description: "Open specific nodes in the knowledge graph by their names",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "names": map[string]interface{}{
- "type": "array",
- "items": map[string]interface{}{
- "type": "string",
+ "description": "An array of observation contents associated with the entity",
},
- "description": "An array of entity names to retrieve",
},
+ "required": []string{"name", "entityType", "observations"},
},
- "required": []string{"names"},
},
},
- }
-}
-
-// Tool handlers
-
-func (ms *Server) HandleCreateEntities(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- // Ensure graph is loaded before accessing
- if err := ms.ensureGraphLoaded(); err != nil {
- return mcp.NewToolError(fmt.Sprintf("Failed to load graph: %v", err)), nil
- }
-
- ms.mu.Lock()
- defer ms.mu.Unlock()
-
- entitiesArg, ok := req.Arguments["entities"]
- if !ok {
- return mcp.NewToolError("entities parameter is required"), nil
- }
-
- entitiesSlice, ok := entitiesArg.([]interface{})
- if !ok {
- return mcp.NewToolError("entities must be an array"), nil
- }
-
- var createdEntities []string
-
- for _, entityArg := range entitiesSlice {
- entityMap, ok := entityArg.(map[string]interface{})
- if !ok {
- return mcp.NewToolError("each entity must be an object"), nil
+ "required": []string{"entities"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := memory.ensureGraphLoaded(); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to load graph: %v", err)), nil
}
- name, ok := entityMap["name"].(string)
- if !ok {
- return mcp.NewToolError("entity name must be a string"), nil
- }
+ memory.mu.Lock()
+ defer memory.mu.Unlock()
- entityType, ok := entityMap["entityType"].(string)
+ entitiesArg, ok := req.Arguments["entities"]
if !ok {
- return mcp.NewToolError("entity entityType must be a string"), nil
+ return mcp.NewToolError("entities parameter is required"), nil
}
- observationsArg, ok := entityMap["observations"]
+ entitiesSlice, ok := entitiesArg.([]interface{})
if !ok {
- return mcp.NewToolError("entity observations is required"), nil
+ return mcp.NewToolError("entities must be an array"), nil
}
- observationsSlice, ok := observationsArg.([]interface{})
- if !ok {
- return mcp.NewToolError("entity observations must be an array"), nil
- }
+ var createdEntities []string
- var observations []string
- for _, obs := range observationsSlice {
- obsStr, ok := obs.(string)
+ for _, entityArg := range entitiesSlice {
+ entityMap, ok := entityArg.(map[string]interface{})
if !ok {
- return mcp.NewToolError("each observation must be a string"), nil
+ return mcp.NewToolError("each entity must be an object"), nil
}
- observations = append(observations, obsStr)
- }
- // Create entity
- entity := &Entity{
- Name: name,
- EntityType: entityType,
- Observations: observations,
- }
+ name, ok := entityMap["name"].(string)
+ if !ok {
+ return mcp.NewToolError("entity name must be a string"), nil
+ }
- ms.graph.Entities[name] = entity
- createdEntities = append(createdEntities, name)
- }
+ entityType, ok := entityMap["entityType"].(string)
+ if !ok {
+ return mcp.NewToolError("entity type must be a string"), nil
+ }
- // Save to file
- if err := ms.saveGraph(); err != nil {
- return mcp.NewToolError(fmt.Sprintf("Failed to save graph: %v", err)), nil
- }
+ observationsArg, ok := entityMap["observations"]
+ if !ok {
+ return mcp.NewToolError("entity observations are required"), nil
+ }
- result := fmt.Sprintf("Successfully created %d entities: %s", len(createdEntities), strings.Join(createdEntities, ", "))
- return mcp.NewToolResult(mcp.NewTextContent(result)), nil
-}
+ observationsSlice, ok := observationsArg.([]interface{})
+ if !ok {
+ return mcp.NewToolError("entity observations must be an array"), nil
+ }
-func (ms *Server) HandleCreateRelations(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- ms.mu.Lock()
- defer ms.mu.Unlock()
+ var observations []string
+ for _, obs := range observationsSlice {
+ obsStr, ok := obs.(string)
+ if !ok {
+ return mcp.NewToolError("each observation must be a string"), nil
+ }
+ observations = append(observations, obsStr)
+ }
- relationsArg, ok := req.Arguments["relations"]
- if !ok {
- return mcp.NewToolError("relations parameter is required"), nil
- }
+ memory.graph.Entities[name] = &Entity{
+ Name: name,
+ EntityType: entityType,
+ Observations: observations,
+ }
+ createdEntities = append(createdEntities, name)
+ }
- relationsSlice, ok := relationsArg.([]interface{})
- if !ok {
- return mcp.NewToolError("relations must be an array"), nil
- }
+ if err := memory.saveGraph(); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to save graph: %v", err)), nil
+ }
- var createdRelations []string
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Created %d entities: %s", len(createdEntities), strings.Join(createdEntities, ", ")))), nil
+ }))
- for _, relationArg := range relationsSlice {
- relationMap, ok := relationArg.(map[string]interface{})
- if !ok {
- return mcp.NewToolError("each relation must be an object"), nil
+ // Add create_relations tool
+ builder.AddTool(mcp.NewTool("create_relations", "Create multiple new relations between entities in the knowledge graph. Relations should be in active voice", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "relations": map[string]interface{}{
+ "type": "array",
+ "items": map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "from": map[string]interface{}{
+ "type": "string",
+ "description": "The name of the entity where the relation starts",
+ },
+ "to": map[string]interface{}{
+ "type": "string",
+ "description": "The name of the entity where the relation ends",
+ },
+ "relationType": map[string]interface{}{
+ "type": "string",
+ "description": "The type of the relation",
+ },
+ },
+ "required": []string{"from", "to", "relationType"},
+ },
+ },
+ },
+ "required": []string{"relations"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := memory.ensureGraphLoaded(); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to load graph: %v", err)), nil
}
- from, ok := relationMap["from"].(string)
- if !ok {
- return mcp.NewToolError("relation from must be a string"), nil
- }
+ memory.mu.Lock()
+ defer memory.mu.Unlock()
- to, ok := relationMap["to"].(string)
+ relationsArg, ok := req.Arguments["relations"]
if !ok {
- return mcp.NewToolError("relation to must be a string"), nil
+ return mcp.NewToolError("relations parameter is required"), nil
}
- relationType, ok := relationMap["relationType"].(string)
+ relationsSlice, ok := relationsArg.([]interface{})
if !ok {
- return mcp.NewToolError("relation relationType must be a string"), nil
+ return mcp.NewToolError("relations must be an array"), nil
}
- // Check that entities exist
- if _, exists := ms.graph.Entities[from]; !exists {
- return mcp.NewToolError(fmt.Sprintf("entity '%s' does not exist", from)), nil
- }
-
- if _, exists := ms.graph.Entities[to]; !exists {
- return mcp.NewToolError(fmt.Sprintf("entity '%s' does not exist", to)), nil
- }
+ var createdRelations []string
- // Create relation key
- relationKey := fmt.Sprintf("%s-%s-%s", from, relationType, to)
+ for _, relationArg := range relationsSlice {
+ relationMap, ok := relationArg.(map[string]interface{})
+ if !ok {
+ return mcp.NewToolError("each relation must be an object"), nil
+ }
- // Create relation
- relation := Relation{
- From: from,
- To: to,
- RelationType: relationType,
- }
+ from, ok := relationMap["from"].(string)
+ if !ok {
+ return mcp.NewToolError("relation 'from' must be a string"), nil
+ }
- ms.graph.Relations[relationKey] = relation
- createdRelations = append(createdRelations, fmt.Sprintf("%s %s %s", from, relationType, to))
- }
+ to, ok := relationMap["to"].(string)
+ if !ok {
+ return mcp.NewToolError("relation 'to' must be a string"), nil
+ }
- // Save to file
- if err := ms.saveGraph(); err != nil {
- return mcp.NewToolError(fmt.Sprintf("Failed to save graph: %v", err)), nil
- }
+ relationType, ok := relationMap["relationType"].(string)
+ if !ok {
+ return mcp.NewToolError("relation type must be a string"), nil
+ }
- result := fmt.Sprintf("Successfully created %d relations: %s", len(createdRelations), strings.Join(createdRelations, ", "))
- return mcp.NewToolResult(mcp.NewTextContent(result)), nil
-}
+ if _, exists := memory.graph.Entities[from]; !exists {
+ return mcp.NewToolError(fmt.Sprintf("entity '%s' does not exist", from)), nil
+ }
-func (ms *Server) HandleAddObservations(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- ms.mu.Lock()
- defer ms.mu.Unlock()
+ if _, exists := memory.graph.Entities[to]; !exists {
+ return mcp.NewToolError(fmt.Sprintf("entity '%s' does not exist", to)), nil
+ }
- observationsArg, ok := req.Arguments["observations"]
- if !ok {
- return mcp.NewToolError("observations parameter is required"), nil
- }
+ relationKey := fmt.Sprintf("%s-%s-%s", from, relationType, to)
+ memory.graph.Relations[relationKey] = Relation{
+ From: from,
+ To: to,
+ RelationType: relationType,
+ }
+ createdRelations = append(createdRelations, fmt.Sprintf("%s %s %s", from, relationType, to))
+ }
- observationsSlice, ok := observationsArg.([]interface{})
- if !ok {
- return mcp.NewToolError("observations must be an array"), nil
- }
+ if err := memory.saveGraph(); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to save graph: %v", err)), nil
+ }
- var addedCount int
- var addedObservations []string
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Created %d relations: %s", len(createdRelations), strings.Join(createdRelations, ", ")))), nil
+ }))
- for _, observationArg := range observationsSlice {
- observationMap, ok := observationArg.(map[string]interface{})
- if !ok {
- return mcp.NewToolError("each observation must be an object"), nil
+ // Add add_observations tool
+ builder.AddTool(mcp.NewTool("add_observations", "Add new observations to existing entities in the knowledge graph", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "observations": map[string]interface{}{
+ "type": "array",
+ "items": map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "entityName": map[string]interface{}{
+ "type": "string",
+ "description": "The name of the entity to add the observations to",
+ },
+ "contents": map[string]interface{}{
+ "type": "array",
+ "items": map[string]interface{}{
+ "type": "string",
+ },
+ "description": "An array of observation contents to add",
+ },
+ },
+ "required": []string{"entityName", "contents"},
+ },
+ },
+ },
+ "required": []string{"observations"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := memory.ensureGraphLoaded(); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to load graph: %v", err)), nil
}
- entityName, ok := observationMap["entityName"].(string)
- if !ok {
- return mcp.NewToolError("observation entityName must be a string"), nil
- }
+ memory.mu.Lock()
+ defer memory.mu.Unlock()
- contentsArg, ok := observationMap["contents"]
+ observationsArg, ok := req.Arguments["observations"]
if !ok {
- return mcp.NewToolError("observation contents is required"), nil
+ return mcp.NewToolError("observations parameter is required"), nil
}
- contentsSlice, ok := contentsArg.([]interface{})
+ observationsSlice, ok := observationsArg.([]interface{})
if !ok {
- return mcp.NewToolError("observation contents must be an array"), nil
+ return mcp.NewToolError("observations must be an array"), nil
}
- // Check that entity exists
- entity, exists := ms.graph.Entities[entityName]
- if !exists {
- return mcp.NewToolError(fmt.Sprintf("entity '%s' does not exist", entityName)), nil
- }
+ var addedCount int
- // Add observations
- for _, content := range contentsSlice {
- contentStr, ok := content.(string)
+ for _, obsArg := range observationsSlice {
+ obsMap, ok := obsArg.(map[string]interface{})
if !ok {
- return mcp.NewToolError("each observation content must be a string"), nil
+ return mcp.NewToolError("each observation must be an object"), nil
}
- entity.Observations = append(entity.Observations, contentStr)
- addedObservations = append(addedObservations, contentStr)
- addedCount++
- }
- }
- // Save to file
- if err := ms.saveGraph(); err != nil {
- return mcp.NewToolError(fmt.Sprintf("Failed to save graph: %v", err)), nil
- }
+ entityName, ok := obsMap["entityName"].(string)
+ if !ok {
+ return mcp.NewToolError("entity name must be a string"), nil
+ }
- result := fmt.Sprintf("Successfully added %d observations: %s", addedCount, strings.Join(addedObservations, ", "))
- return mcp.NewToolResult(mcp.NewTextContent(result)), nil
-}
+ contentsArg, ok := obsMap["contents"]
+ if !ok {
+ return mcp.NewToolError("observation contents are required"), nil
+ }
-func (ms *Server) HandleDeleteEntities(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- ms.mu.Lock()
- defer ms.mu.Unlock()
+ contentsSlice, ok := contentsArg.([]interface{})
+ if !ok {
+ return mcp.NewToolError("observation contents must be an array"), nil
+ }
- entityNamesArg, ok := req.Arguments["entityNames"]
- if !ok {
- return mcp.NewToolError("entityNames parameter is required"), nil
- }
+ entity, exists := memory.graph.Entities[entityName]
+ if !exists {
+ return mcp.NewToolError(fmt.Sprintf("entity '%s' does not exist", entityName)), nil
+ }
- entityNamesSlice, ok := entityNamesArg.([]interface{})
- if !ok {
- return mcp.NewToolError("entityNames must be an array"), nil
- }
+ for _, content := range contentsSlice {
+ contentStr, ok := content.(string)
+ if !ok {
+ return mcp.NewToolError("each observation content must be a string"), nil
+ }
+ entity.Observations = append(entity.Observations, contentStr)
+ addedCount++
+ }
+ }
- var deletedEntities []string
+ if err := memory.saveGraph(); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to save graph: %v", err)), nil
+ }
- for _, entityNameArg := range entityNamesSlice {
- entityName, ok := entityNameArg.(string)
- if !ok {
- return mcp.NewToolError("each entity name must be a string"), nil
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Added %d observations", addedCount))), nil
+ }))
+
+ // Add delete_entities tool
+ builder.AddTool(mcp.NewTool("delete_entities", "Delete multiple entities and their associated relations from the knowledge graph", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "entityNames": map[string]interface{}{
+ "type": "array",
+ "items": map[string]interface{}{
+ "type": "string",
+ },
+ "description": "An array of entity names to delete",
+ },
+ },
+ "required": []string{"entityNames"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := memory.ensureGraphLoaded(); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to load graph: %v", err)), nil
}
- // Delete entity
- if _, exists := ms.graph.Entities[entityName]; exists {
- delete(ms.graph.Entities, entityName)
- deletedEntities = append(deletedEntities, entityName)
+ memory.mu.Lock()
+ defer memory.mu.Unlock()
- // Delete related relations
- for key, relation := range ms.graph.Relations {
- if relation.From == entityName || relation.To == entityName {
- delete(ms.graph.Relations, key)
- }
- }
+ entityNamesArg, ok := req.Arguments["entityNames"]
+ if !ok {
+ return mcp.NewToolError("entityNames parameter is required"), nil
}
- }
- // Save to file
- if err := ms.saveGraph(); err != nil {
- return mcp.NewToolError(fmt.Sprintf("Failed to save graph: %v", err)), nil
- }
+ entityNamesSlice, ok := entityNamesArg.([]interface{})
+ if !ok {
+ return mcp.NewToolError("entityNames must be an array"), nil
+ }
- result := fmt.Sprintf("Successfully deleted %d entities: %s", len(deletedEntities), strings.Join(deletedEntities, ", "))
- return mcp.NewToolResult(mcp.NewTextContent(result)), nil
-}
+ var deletedEntities []string
+ var deletedRelations int
-func (ms *Server) HandleDeleteObservations(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- ms.mu.Lock()
- defer ms.mu.Unlock()
+ for _, nameArg := range entityNamesSlice {
+ name, ok := nameArg.(string)
+ if !ok {
+ return mcp.NewToolError("each entity name must be a string"), nil
+ }
- deletionsArg, ok := req.Arguments["deletions"]
- if !ok {
- return mcp.NewToolError("deletions parameter is required"), nil
- }
+ if _, exists := memory.graph.Entities[name]; !exists {
+ continue
+ }
- deletionsSlice, ok := deletionsArg.([]interface{})
- if !ok {
- return mcp.NewToolError("deletions must be an array"), nil
- }
+ delete(memory.graph.Entities, name)
+ deletedEntities = append(deletedEntities, name)
- var deletedCount int
+ for key, relation := range memory.graph.Relations {
+ if relation.From == name || relation.To == name {
+ delete(memory.graph.Relations, key)
+ deletedRelations++
+ }
+ }
+ }
- for _, deletionArg := range deletionsSlice {
- deletionMap, ok := deletionArg.(map[string]interface{})
- if !ok {
- return mcp.NewToolError("each deletion must be an object"), nil
+ if err := memory.saveGraph(); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to save graph: %v", err)), nil
}
- entityName, ok := deletionMap["entityName"].(string)
- if !ok {
- return mcp.NewToolError("deletion entityName must be a string"), nil
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Deleted %d entities and %d relations", len(deletedEntities), deletedRelations))), nil
+ }))
+
+ // Add delete_observations tool
+ builder.AddTool(mcp.NewTool("delete_observations", "Delete specific observations from entities in the knowledge graph", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "deletions": map[string]interface{}{
+ "type": "array",
+ "items": map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "entityName": map[string]interface{}{
+ "type": "string",
+ "description": "The name of the entity containing the observations",
+ },
+ "observations": map[string]interface{}{
+ "type": "array",
+ "items": map[string]interface{}{
+ "type": "string",
+ },
+ "description": "An array of observations to delete",
+ },
+ },
+ "required": []string{"entityName", "observations"},
+ },
+ },
+ },
+ "required": []string{"deletions"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := memory.ensureGraphLoaded(); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to load graph: %v", err)), nil
}
- observationsArg, ok := deletionMap["observations"]
+ memory.mu.Lock()
+ defer memory.mu.Unlock()
+
+ deletionsArg, ok := req.Arguments["deletions"]
if !ok {
- return mcp.NewToolError("deletion observations is required"), nil
+ return mcp.NewToolError("deletions parameter is required"), nil
}
- observationsSlice, ok := observationsArg.([]interface{})
+ deletionsSlice, ok := deletionsArg.([]interface{})
if !ok {
- return mcp.NewToolError("deletion observations must be an array"), nil
+ return mcp.NewToolError("deletions must be an array"), nil
}
- // Check that entity exists
- entity, exists := ms.graph.Entities[entityName]
- if !exists {
- return mcp.NewToolError(fmt.Sprintf("entity '%s' does not exist", entityName)), nil
- }
+ var deletedCount int
- // Delete observations
- for _, obsArg := range observationsSlice {
- obsStr, ok := obsArg.(string)
+ for _, delArg := range deletionsSlice {
+ delMap, ok := delArg.(map[string]interface{})
if !ok {
- return mcp.NewToolError("each observation must be a string"), nil
+ return mcp.NewToolError("each deletion must be an object"), nil
}
- // Remove observation from entity
- var newObservations []string
- for _, existingObs := range entity.Observations {
- if existingObs != obsStr {
- newObservations = append(newObservations, existingObs)
- } else {
- deletedCount++
- }
+ entityName, ok := delMap["entityName"].(string)
+ if !ok {
+ return mcp.NewToolError("entity name must be a string"), nil
}
- entity.Observations = newObservations
- }
- }
-
- // Save to file
- if err := ms.saveGraph(); err != nil {
- return mcp.NewToolError(fmt.Sprintf("Failed to save graph: %v", err)), nil
- }
- result := fmt.Sprintf("Successfully deleted %d observations", deletedCount)
- return mcp.NewToolResult(mcp.NewTextContent(result)), nil
-}
+ observationsArg, ok := delMap["observations"]
+ if !ok {
+ return mcp.NewToolError("observations are required"), nil
+ }
-func (ms *Server) HandleDeleteRelations(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- ms.mu.Lock()
- defer ms.mu.Unlock()
+ observationsSlice, ok := observationsArg.([]interface{})
+ if !ok {
+ return mcp.NewToolError("observations must be an array"), nil
+ }
- relationsArg, ok := req.Arguments["relations"]
- if !ok {
- return mcp.NewToolError("relations parameter is required"), nil
- }
+ entity, exists := memory.graph.Entities[entityName]
+ if !exists {
+ continue
+ }
- relationsSlice, ok := relationsArg.([]interface{})
- if !ok {
- return mcp.NewToolError("relations must be an array"), nil
- }
+ for _, obsArg := range observationsSlice {
+ obsStr, ok := obsArg.(string)
+ if !ok {
+ continue
+ }
- var deletedRelations []string
+ for i, existingObs := range entity.Observations {
+ if existingObs == obsStr {
+ entity.Observations = append(entity.Observations[:i], entity.Observations[i+1:]...)
+ deletedCount++
+ break
+ }
+ }
+ }
+ }
- for _, relationArg := range relationsSlice {
- relationMap, ok := relationArg.(map[string]interface{})
- if !ok {
- return mcp.NewToolError("each relation must be an object"), nil
+ if err := memory.saveGraph(); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to save graph: %v", err)), nil
}
- from, ok := relationMap["from"].(string)
- if !ok {
- return mcp.NewToolError("relation from must be a string"), nil
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Deleted %d observations", deletedCount))), nil
+ }))
+
+ // Add delete_relations tool
+ builder.AddTool(mcp.NewTool("delete_relations", "Delete multiple relations from the knowledge graph", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "relations": map[string]interface{}{
+ "type": "array",
+ "items": map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "from": map[string]interface{}{
+ "type": "string",
+ "description": "The name of the entity where the relation starts",
+ },
+ "to": map[string]interface{}{
+ "type": "string",
+ "description": "The name of the entity where the relation ends",
+ },
+ "relationType": map[string]interface{}{
+ "type": "string",
+ "description": "The type of the relation",
+ },
+ },
+ "required": []string{"from", "to", "relationType"},
+ },
+ },
+ },
+ "required": []string{"relations"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := memory.ensureGraphLoaded(); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to load graph: %v", err)), nil
}
- to, ok := relationMap["to"].(string)
+ memory.mu.Lock()
+ defer memory.mu.Unlock()
+
+ relationsArg, ok := req.Arguments["relations"]
if !ok {
- return mcp.NewToolError("relation to must be a string"), nil
+ return mcp.NewToolError("relations parameter is required"), nil
}
- relationType, ok := relationMap["relationType"].(string)
+ relationsSlice, ok := relationsArg.([]interface{})
if !ok {
- return mcp.NewToolError("relation relationType must be a string"), nil
+ return mcp.NewToolError("relations must be an array"), nil
}
- // Create relation key
- relationKey := fmt.Sprintf("%s-%s-%s", from, relationType, to)
+ var deletedCount int
- // Delete relation
- if _, exists := ms.graph.Relations[relationKey]; exists {
- delete(ms.graph.Relations, relationKey)
- deletedRelations = append(deletedRelations, fmt.Sprintf("%s %s %s", from, relationType, to))
- }
- }
-
- // Save to file
- if err := ms.saveGraph(); err != nil {
- return mcp.NewToolError(fmt.Sprintf("Failed to save graph: %v", err)), nil
- }
+ for _, relationArg := range relationsSlice {
+ relationMap, ok := relationArg.(map[string]interface{})
+ if !ok {
+ continue
+ }
- result := fmt.Sprintf("Successfully deleted %d relations: %s", len(deletedRelations), strings.Join(deletedRelations, ", "))
- return mcp.NewToolResult(mcp.NewTextContent(result)), nil
-}
+ from, ok := relationMap["from"].(string)
+ if !ok {
+ continue
+ }
-func (ms *Server) HandleReadGraph(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- // Ensure graph is loaded before accessing
- if err := ms.ensureGraphLoaded(); err != nil {
- return mcp.NewToolError(fmt.Sprintf("Failed to load graph: %v", err)), nil
- }
+ to, ok := relationMap["to"].(string)
+ if !ok {
+ continue
+ }
- ms.mu.RLock()
- defer ms.mu.RUnlock()
+ relationType, ok := relationMap["relationType"].(string)
+ if !ok {
+ continue
+ }
- // Return the entire graph as JSON
- graphJSON, err := json.MarshalIndent(ms.graph, "", " ")
- if err != nil {
- return mcp.NewToolError(fmt.Sprintf("Failed to marshal graph: %v", err)), nil
- }
+ relationKey := fmt.Sprintf("%s-%s-%s", from, relationType, to)
+ if _, exists := memory.graph.Relations[relationKey]; exists {
+ delete(memory.graph.Relations, relationKey)
+ deletedCount++
+ }
+ }
- return mcp.NewToolResult(mcp.NewTextContent(string(graphJSON))), nil
-}
+ if err := memory.saveGraph(); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to save graph: %v", err)), nil
+ }
-func (ms *Server) HandleSearchNodes(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- ms.mu.RLock()
- defer ms.mu.RUnlock()
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Deleted %d relations", deletedCount))), nil
+ }))
- query, ok := req.Arguments["query"].(string)
- if !ok {
- return mcp.NewToolError("query parameter is required and must be a string"), nil
- }
+ // Add read_graph tool
+ builder.AddTool(mcp.NewTool("read_graph", "Read the entire knowledge graph", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := memory.ensureGraphLoaded(); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to load graph: %v", err)), nil
+ }
- query = strings.ToLower(query)
- var matchedEntities []*Entity
+ memory.mu.RLock()
+ defer memory.mu.RUnlock()
- // Search entities
- for _, entity := range ms.graph.Entities {
- // Check name
- if strings.Contains(strings.ToLower(entity.Name), query) {
- matchedEntities = append(matchedEntities, entity)
- continue
+ graphData, err := json.MarshalIndent(memory.graph, "", " ")
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to serialize graph: %v", err)), nil
}
- // Check type
- if strings.Contains(strings.ToLower(entity.EntityType), query) {
- matchedEntities = append(matchedEntities, entity)
- continue
+ return mcp.NewToolResult(mcp.NewTextContent(string(graphData))), nil
+ }))
+
+ // Add search_nodes tool
+ builder.AddTool(mcp.NewTool("search_nodes", "Search for nodes in the knowledge graph based on a query", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "query": map[string]interface{}{
+ "type": "string",
+ "description": "The search query to match against entity names, types, and observation content",
+ },
+ },
+ "required": []string{"query"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := memory.ensureGraphLoaded(); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to load graph: %v", err)), nil
}
- // Check observations
- for _, obs := range entity.Observations {
- if strings.Contains(strings.ToLower(obs), query) {
- matchedEntities = append(matchedEntities, entity)
- break
- }
+ memory.mu.RLock()
+ defer memory.mu.RUnlock()
+
+ query, ok := req.Arguments["query"].(string)
+ if !ok {
+ return mcp.NewToolError("query parameter is required"), nil
}
- }
- // Return matched entities as JSON
- searchResult := map[string]interface{}{
- "query": query,
- "entities": matchedEntities,
- }
+ query = strings.ToLower(query)
+ var matchedEntities []*Entity
- resultJSON, err := json.MarshalIndent(searchResult, "", " ")
- if err != nil {
- return mcp.NewToolError(fmt.Sprintf("Failed to marshal search result: %v", err)), nil
- }
+ for _, entity := range memory.graph.Entities {
+ if strings.Contains(strings.ToLower(entity.Name), query) ||
+ strings.Contains(strings.ToLower(entity.EntityType), query) {
+ matchedEntities = append(matchedEntities, entity)
+ continue
+ }
- return mcp.NewToolResult(mcp.NewTextContent(string(resultJSON))), nil
-}
+ for _, observation := range entity.Observations {
+ if strings.Contains(strings.ToLower(observation), query) {
+ matchedEntities = append(matchedEntities, entity)
+ break
+ }
+ }
+ }
-func (ms *Server) HandleOpenNodes(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- ms.mu.RLock()
- defer ms.mu.RUnlock()
+ resultData, err := json.MarshalIndent(matchedEntities, "", " ")
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to serialize results: %v", err)), nil
+ }
- namesArg, ok := req.Arguments["names"]
- if !ok {
- return mcp.NewToolError("names parameter is required"), nil
- }
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Found %d matching entities:\n%s", len(matchedEntities), string(resultData)))), nil
+ }))
- namesSlice, ok := namesArg.([]interface{})
- if !ok {
- return mcp.NewToolError("names must be an array"), nil
- }
+ // Add open_nodes tool
+ builder.AddTool(mcp.NewTool("open_nodes", "Open specific nodes in the knowledge graph by their names", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "names": map[string]interface{}{
+ "type": "array",
+ "items": map[string]interface{}{
+ "type": "string",
+ },
+ "description": "An array of entity names to retrieve",
+ },
+ },
+ "required": []string{"names"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ if err := memory.ensureGraphLoaded(); err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to load graph: %v", err)), nil
+ }
- var foundEntities []*Entity
+ memory.mu.RLock()
+ defer memory.mu.RUnlock()
- for _, nameArg := range namesSlice {
- name, ok := nameArg.(string)
+ namesArg, ok := req.Arguments["names"]
if !ok {
- return mcp.NewToolError("each name must be a string"), nil
+ return mcp.NewToolError("names parameter is required"), nil
}
- if entity, exists := ms.graph.Entities[name]; exists {
- foundEntities = append(foundEntities, entity)
+ namesSlice, ok := namesArg.([]interface{})
+ if !ok {
+ return mcp.NewToolError("names must be an array"), nil
}
- }
-
- // Return found entities as JSON
- openResult := map[string]interface{}{
- "entities": foundEntities,
- }
- resultJSON, err := json.MarshalIndent(openResult, "", " ")
- if err != nil {
- return mcp.NewToolError(fmt.Sprintf("Failed to marshal open result: %v", err)), nil
- }
-
- return mcp.NewToolResult(mcp.NewTextContent(string(resultJSON))), nil
-}
+ var foundEntities []*Entity
-// HandleMemoryResource handles memory:// resource requests
-func (ms *Server) HandleMemoryResource(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
- ms.mu.RLock()
- defer ms.mu.RUnlock()
+ for _, nameArg := range namesSlice {
+ name, ok := nameArg.(string)
+ if !ok {
+ continue
+ }
- // Parse memory:// URI: memory://entity/name or memory://relations/all
- if !strings.HasPrefix(req.URI, "memory://") {
- return mcp.ReadResourceResult{}, fmt.Errorf("invalid memory URI: %s", req.URI)
- }
+ if entity, exists := memory.graph.Entities[name]; exists {
+ foundEntities = append(foundEntities, entity)
+ }
+ }
- uriPath := req.URI[9:] // Remove "memory://" prefix
- parts := strings.Split(uriPath, "/")
+ resultData, err := json.MarshalIndent(foundEntities, "", " ")
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to serialize results: %v", err)), nil
+ }
- if len(parts) < 2 {
- return mcp.ReadResourceResult{}, fmt.Errorf("invalid memory URI format: %s", req.URI)
- }
+ return mcp.NewToolResult(mcp.NewTextContent(fmt.Sprintf("Found %d entities:\n%s", len(foundEntities), string(resultData)))), nil
+ }))
- resourceType := parts[0]
- resourcePath := strings.Join(parts[1:], "/")
+ // Add knowledge-query prompt
+ builder.AddPrompt(mcp.NewPrompt("knowledge-query", "Prompt for querying and exploring the knowledge graph", []mcp.PromptArgument{
+ {
+ Name: "query",
+ Description: "What you want to search for or ask about in the knowledge graph",
+ Required: true,
+ },
+ {
+ Name: "context",
+ Description: "Additional context about your question (optional)",
+ Required: false,
+ },
+ }, func(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
+ query, hasQuery := req.Arguments["query"].(string)
+ context, hasContext := req.Arguments["context"].(string)
- switch resourceType {
- case "entity":
- return ms.handleEntityResource(resourcePath)
- case "relations":
- return ms.handleRelationsResource(resourcePath)
- default:
- return mcp.ReadResourceResult{}, fmt.Errorf("unknown memory resource type: %s", resourceType)
- }
-}
+ if !hasQuery || query == "" {
+ return mcp.GetPromptResult{}, fmt.Errorf("query argument is required")
+ }
-// handleEntityResource handles memory://entity/name resources
-func (ms *Server) handleEntityResource(entityName string) (mcp.ReadResourceResult, error) {
- entity, exists := ms.graph.Entities[entityName]
- if !exists {
- return mcp.ReadResourceResult{}, fmt.Errorf("entity not found: %s", entityName)
- }
+ var messages []mcp.PromptMessage
- // Create detailed entity information including related relations
- var relatedRelations []Relation
- for _, relation := range ms.graph.Relations {
- if relation.From == entityName || relation.To == entityName {
- relatedRelations = append(relatedRelations, relation)
+ userContent := fmt.Sprintf(`I want to search the knowledge graph for: %s`, query)
+ if hasContext && context != "" {
+ userContent += fmt.Sprintf("\n\nAdditional context: %s", context)
}
- }
- entityInfo := map[string]interface{}{
- "entity": entity,
- "relations": relatedRelations,
- }
+ messages = append(messages, mcp.PromptMessage{
+ Role: "user",
+ Content: mcp.NewTextContent(userContent),
+ })
- jsonData, err := json.MarshalIndent(entityInfo, "", " ")
- if err != nil {
- return mcp.ReadResourceResult{}, fmt.Errorf("failed to marshal entity data: %v", err)
- }
+ assistantContent := fmt.Sprintf(`I'll help you search the knowledge graph for "%s". Here are some strategies you can use:
- return mcp.ReadResourceResult{
- Contents: []mcp.Content{
- mcp.NewTextContent(string(jsonData)),
- },
- }, nil
-}
+**Search Commands:**
+- Use "search_nodes" tool with query: "%s"
+- Use "read_graph" tool to see the entire knowledge structure
+- Use "open_nodes" tool if you know specific entity names
-// handleRelationsResource handles memory://relations/all resources
-func (ms *Server) handleRelationsResource(resourcePath string) (mcp.ReadResourceResult, error) {
- if resourcePath != "all" {
- return mcp.ReadResourceResult{}, fmt.Errorf("invalid relations resource path: %s", resourcePath)
- }
+**What to look for:**
+- Entities with names containing "%s"
+- Entity types that match your query
+- Observations that mention "%s"
+- Related entities through relationships
- relationsInfo := map[string]interface{}{
- "total_relations": len(ms.graph.Relations),
- "relations": ms.graph.Relations,
- }
+**Next steps:**
+1. Start with a broad search using search_nodes
+2. Examine the results to find relevant entities
+3. Use open_nodes to get detailed information about specific entities
+4. Look at relationships to find connected information
- jsonData, err := json.MarshalIndent(relationsInfo, "", " ")
- if err != nil {
- return mcp.ReadResourceResult{}, fmt.Errorf("failed to marshal relations data: %v", err)
- }
+Would you like me to search for this information in the knowledge graph?`, query, query, query, query)
- return mcp.ReadResourceResult{
- Contents: []mcp.Content{
- mcp.NewTextContent(string(jsonData)),
- },
- }, nil
-}
+ messages = append(messages, mcp.PromptMessage{
+ Role: "assistant",
+ Content: mcp.NewTextContent(assistantContent),
+ })
-// Prompt handlers
+ description := fmt.Sprintf("Knowledge graph search guidance for: %s", query)
-func (ms *Server) HandleKnowledgeQueryPrompt(req mcp.GetPromptRequest) (mcp.GetPromptResult, error) {
- query, hasQuery := req.Arguments["query"].(string)
- context, hasContext := req.Arguments["context"].(string)
+ return mcp.GetPromptResult{
+ Description: description,
+ Messages: messages,
+ }, nil
+ }))
- if !hasQuery || query == "" {
- return mcp.GetPromptResult{}, fmt.Errorf("query argument is required")
- }
+ // Add memory:// pattern resource
+ builder.AddResource(mcp.NewResource(
+ "memory://graph",
+ "Knowledge Graph",
+ "application/json",
+ func(req mcp.ReadResourceRequest) (mcp.ReadResourceResult, error) {
+ if err := memory.ensureGraphLoaded(); err != nil {
+ return mcp.ReadResourceResult{}, fmt.Errorf("failed to load graph: %v", err)
+ }
- // Create the prompt messages
- var messages []mcp.PromptMessage
+ memory.mu.RLock()
+ defer memory.mu.RUnlock()
- // User message with the query and optional context
- userContent := fmt.Sprintf("I want to query the knowledge graph: %s", query)
- if hasContext && context != "" {
- userContent += fmt.Sprintf("\n\nAdditional context: %s", context)
- }
+ graphData, err := json.MarshalIndent(memory.graph, "", " ")
+ if err != nil {
+ return mcp.ReadResourceResult{}, fmt.Errorf("failed to serialize graph: %v", err)
+ }
- messages = append(messages, mcp.PromptMessage{
- Role: "user",
- Content: mcp.NewTextContent(userContent),
- })
+ return mcp.ReadResourceResult{
+ Contents: []mcp.Content{
+ mcp.NewTextContent(string(graphData)),
+ },
+ }, nil
+ },
+ ))
- // Assistant message with guidance on using the memory tools
- assistantContent := fmt.Sprintf(`I'll help you explore the knowledge graph to answer your query: "%s"
+ // Add knowledge graph root
+ rootName := fmt.Sprintf("Knowledge Graph (%d entities, %d relations)", len(memory.graph.Entities), len(memory.graph.Relations))
+ builder.AddRoot(mcp.NewRoot("memory://graph", rootName))
-Let me start by searching the knowledge graph for relevant information:`, query)
+ return builder.Build()
+}
- if hasContext && context != "" {
- assistantContent += fmt.Sprintf("\n\nWith context: %s", context)
- }
+// Helper methods for MemoryOperations
- messages = append(messages, mcp.PromptMessage{
- Role: "assistant",
- Content: mcp.NewTextContent(assistantContent),
- })
+func (memory *MemoryOperations) ensureGraphLoaded() error {
+ memory.mu.Lock()
+ defer memory.mu.Unlock()
- description := fmt.Sprintf("Knowledge graph exploration for: %s", query)
- if hasContext && context != "" {
- description += fmt.Sprintf(" (%s)", context)
+ if !memory.loaded {
+ memory.loaded = true
+ return memory.loadGraphInternal()
}
-
- return mcp.GetPromptResult{
- Description: description,
- Messages: messages,
- }, nil
+ return nil
}
-// Helper methods
-
-func (ms *Server) loadGraphInternal() error {
- if _, err := os.Stat(ms.memoryFile); os.IsNotExist(err) {
- // File doesn't exist, start with empty graph
+func (memory *MemoryOperations) loadGraphInternal() error {
+ if memory.memoryFile == "" {
return nil
}
- data, err := os.ReadFile(ms.memoryFile)
+ data, err := os.ReadFile(memory.memoryFile)
+ if os.IsNotExist(err) {
+ return nil
+ }
if err != nil {
- return err
+ return fmt.Errorf("failed to read memory file: %v", err)
}
if len(data) == 0 {
- // Empty file, start with empty graph
return nil
}
- return json.Unmarshal(data, ms.graph)
+ var loadedGraph KnowledgeGraph
+ if err := json.Unmarshal(data, &loadedGraph); err != nil {
+ return fmt.Errorf("failed to parse memory file: %v", err)
+ }
+
+ if loadedGraph.Entities != nil {
+ memory.graph.Entities = loadedGraph.Entities
+ }
+ if loadedGraph.Relations != nil {
+ memory.graph.Relations = loadedGraph.Relations
+ }
+
+ return nil
}
-func (ms *Server) saveGraph() error {
- data, err := json.MarshalIndent(ms.graph, "", " ")
- if err != nil {
- return err
+func (memory *MemoryOperations) saveGraph() error {
+ if memory.memoryFile == "" {
+ return nil
}
- err = os.WriteFile(ms.memoryFile, data, 0644)
+ data, err := json.MarshalIndent(memory.graph, "", " ")
if err != nil {
- return err
+ return fmt.Errorf("failed to serialize graph: %v", err)
+ }
+
+ if err := os.WriteFile(memory.memoryFile, data, 0644); err != nil {
+ return fmt.Errorf("failed to write memory file: %v", err)
}
- // No need to re-register resources since they're discovered dynamically
return nil
-}
+}
\ No newline at end of file
pkg/thinking/server.go
@@ -12,9 +12,8 @@ import (
"github.com/xlgmokha/mcp/pkg/mcp"
)
-// Server implements the Sequential Thinking MCP server
-type Server struct {
- *mcp.Server
+// ThinkingOperations provides thinking session management operations
+type ThinkingOperations struct {
sessions map[string]*ThinkingSession
branches map[string]*Branch
mu sync.RWMutex
@@ -68,17 +67,9 @@ type ThinkingResponse struct {
BranchContext string `json:"branch_context,omitempty"`
}
-// New creates a new Sequential Thinking MCP server
-func New() *Server {
- return NewWithPersistence("")
-}
-
-// NewWithPersistence creates a new Sequential Thinking MCP server with optional persistence
-func NewWithPersistence(persistFile string) *Server {
- server := mcp.NewServer("mcp-sequential-thinking", "1.0.0", []mcp.Tool{}, []mcp.Resource{}, []mcp.Root{})
-
- thinkingServer := &Server{
- Server: server,
+// NewThinkingOperations creates a new ThinkingOperations helper
+func NewThinkingOperations(persistFile string) *ThinkingOperations {
+ thinking := &ThinkingOperations{
sessions: make(map[string]*ThinkingSession),
branches: make(map[string]*Branch),
persistFile: persistFile,
@@ -86,167 +77,145 @@ func NewWithPersistence(persistFile string) *Server {
// Load existing sessions if persistence file is provided
if persistFile != "" {
- thinkingServer.loadSessions()
+ thinking.loadSessions()
}
- // Register all sequential thinking tools
- thinkingServer.registerTools()
-
- return thinkingServer
+ return thinking
}
-// registerTools registers all Sequential Thinking tools with the server
-func (sts *Server) registerTools() {
- // Get all tool definitions from ListTools method
- tools := sts.ListTools()
-
- // Register each tool with its proper definition
- for _, tool := range tools {
- var handler mcp.ToolHandler
- switch tool.Name {
- case "sequentialthinking":
- handler = sts.HandleSequentialThinking
- case "get_session_history":
- handler = sts.HandleGetSessionHistory
- case "list_sessions":
- handler = sts.HandleListSessions
- case "get_branch_history":
- handler = sts.HandleGetBranchHistory
- case "clear_session":
- handler = sts.HandleClearSession
- default:
- continue
- }
- sts.RegisterToolWithDefinition(tool, handler)
- }
+// New creates a new Sequential Thinking MCP server
+func New() *mcp.Server {
+ return NewWithPersistence("")
}
-// ListTools returns all available Sequential Thinking tools
-func (sts *Server) ListTools() []mcp.Tool {
- return []mcp.Tool{
- {
- Name: "sequentialthinking",
- Description: "A detailed tool for dynamic and reflective problem-solving through thoughts. This tool helps analyze problems through a flexible thinking process that can adapt and evolve. Each thought can build on, question, or revise previous insights as understanding deepens.",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "thought": map[string]interface{}{
- "type": "string",
- "description": "Your current thinking step",
- },
- "nextThoughtNeeded": map[string]interface{}{
- "type": "boolean",
- "description": "Whether another thought step is needed",
- },
- "thoughtNumber": map[string]interface{}{
- "type": "integer",
- "minimum": 1,
- "description": "Current thought number",
- },
- "totalThoughts": map[string]interface{}{
- "type": "integer",
- "minimum": 1,
- "description": "Estimated total thoughts needed",
- },
- "isRevision": map[string]interface{}{
- "type": "boolean",
- "description": "Whether this revises previous thinking",
- "default": false,
- },
- "revisesThought": map[string]interface{}{
- "type": "integer",
- "minimum": 1,
- "description": "Which thought is being reconsidered",
- },
- "branchFromThought": map[string]interface{}{
- "type": "integer",
- "minimum": 1,
- "description": "Branching point thought number",
- },
- "branchId": map[string]interface{}{
- "type": "string",
- "description": "Branch identifier",
- },
- "needsMoreThoughts": map[string]interface{}{
- "type": "boolean",
- "description": "If more thoughts are needed",
- "default": false,
- },
- "sessionId": map[string]interface{}{
- "type": "string",
- "description": "Session ID for thought continuity (optional, auto-generated if not provided)",
- },
- },
- "required": []string{"thought", "nextThoughtNeeded", "thoughtNumber", "totalThoughts"},
+// NewWithPersistence creates a new Sequential Thinking MCP server with optional persistence
+func NewWithPersistence(persistFile string) *mcp.Server {
+ thinking := NewThinkingOperations(persistFile)
+ builder := mcp.NewServerBuilder("mcp-sequential-thinking", "1.0.0")
+
+ // Add sequentialthinking tool
+ builder.AddTool(mcp.NewTool("sequentialthinking", "A detailed tool for dynamic and reflective problem-solving through thoughts. This tool helps analyze problems through a flexible thinking process that can adapt and evolve. Each thought can build on, question, or revise previous insights as understanding deepens.", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "thought": map[string]interface{}{
+ "type": "string",
+ "description": "Your current thinking step",
},
- },
- {
- Name: "get_session_history",
- Description: "Get the complete thought history for a thinking session",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "sessionId": map[string]interface{}{
- "type": "string",
- "description": "Session ID to get history for",
- },
- },
- "required": []string{"sessionId"},
+ "nextThoughtNeeded": map[string]interface{}{
+ "type": "boolean",
+ "description": "Whether another thought step is needed",
+ },
+ "thoughtNumber": map[string]interface{}{
+ "type": "integer",
+ "minimum": 1,
+ "description": "Current thought number",
+ },
+ "totalThoughts": map[string]interface{}{
+ "type": "integer",
+ "minimum": 1,
+ "description": "Estimated total thoughts needed",
+ },
+ "isRevision": map[string]interface{}{
+ "type": "boolean",
+ "description": "Whether this revises previous thinking",
+ "default": false,
+ },
+ "revisesThought": map[string]interface{}{
+ "type": "integer",
+ "minimum": 1,
+ "description": "Which thought is being reconsidered",
+ },
+ "branchFromThought": map[string]interface{}{
+ "type": "integer",
+ "minimum": 1,
+ "description": "Branching point thought number",
+ },
+ "branchId": map[string]interface{}{
+ "type": "string",
+ "description": "Branch identifier",
+ },
+ "needsMoreThoughts": map[string]interface{}{
+ "type": "boolean",
+ "description": "If more thoughts are needed",
+ "default": false,
+ },
+ "sessionId": map[string]interface{}{
+ "type": "string",
+ "description": "Session ID for thought continuity (optional, auto-generated if not provided)",
},
},
- {
- Name: "list_sessions",
- Description: "List all active thinking sessions",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{},
+ "required": []string{"thought", "nextThoughtNeeded", "thoughtNumber", "totalThoughts"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return thinking.handleSequentialThinking(req)
+ }))
+
+ // Add get_session_history tool
+ builder.AddTool(mcp.NewTool("get_session_history", "Get the complete thought history for a thinking session", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "sessionId": map[string]interface{}{
+ "type": "string",
+ "description": "Session ID to get history for",
},
},
- {
- Name: "get_branch_history",
- Description: "Get the thought history for a specific reasoning branch",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "branchId": map[string]interface{}{
- "type": "string",
- "description": "Branch ID to get history for",
- },
- },
- "required": []string{"branchId"},
+ "required": []string{"sessionId"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return thinking.handleGetSessionHistory(req)
+ }))
+
+ // Add list_sessions tool
+ builder.AddTool(mcp.NewTool("list_sessions", "List all active thinking sessions", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return thinking.handleListSessions(req)
+ }))
+
+ // Add get_branch_history tool
+ builder.AddTool(mcp.NewTool("get_branch_history", "Get the thought history for a specific reasoning branch", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "branchId": map[string]interface{}{
+ "type": "string",
+ "description": "Branch ID to get history for",
},
},
- {
- Name: "clear_session",
- Description: "Clear a thinking session and all its branches",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "sessionId": map[string]interface{}{
- "type": "string",
- "description": "Session ID to clear",
- },
- },
- "required": []string{"sessionId"},
+ "required": []string{"branchId"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return thinking.handleGetBranchHistory(req)
+ }))
+
+ // Add clear_session tool
+ builder.AddTool(mcp.NewTool("clear_session", "Clear a thinking session and all its branches", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "sessionId": map[string]interface{}{
+ "type": "string",
+ "description": "Session ID to clear",
},
},
- }
+ "required": []string{"sessionId"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ return thinking.handleClearSession(req)
+ }))
+
+ return builder.Build()
}
-// Tool handlers
+// Helper methods for ThinkingOperations
-func (sts *Server) HandleSequentialThinking(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (thinking *ThinkingOperations) handleSequentialThinking(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
// Parse and validate input parameters
- params, err := sts.parseThinkingParameters(req.Arguments)
+ params, err := thinking.parseThinkingParameters(req.Arguments)
if err != nil {
return mcp.NewToolError(err.Error()), nil
}
- sts.mu.Lock()
- defer sts.mu.Unlock()
+ thinking.mu.Lock()
+ defer thinking.mu.Unlock()
// Get or create session
- session := sts.getOrCreateSession(params.SessionID)
+ session := thinking.getOrCreateSession(params.SessionID)
// Create thought object
currentThought := Thought{
@@ -262,7 +231,7 @@ func (sts *Server) HandleSequentialThinking(req mcp.CallToolRequest) (mcp.CallTo
// Handle branching
var activeBranch *Branch
if params.BranchFromThought != nil && params.BranchID != "" {
- activeBranch = sts.getOrCreateBranch(session.ID, params.BranchID, *params.BranchFromThought)
+ activeBranch = thinking.getOrCreateBranch(session.ID, params.BranchID, *params.BranchFromThought)
activeBranch.Thoughts = append(activeBranch.Thoughts, currentThought)
activeBranch.LastActivity = time.Now()
} else {
@@ -276,7 +245,7 @@ func (sts *Server) HandleSequentialThinking(req mcp.CallToolRequest) (mcp.CallTo
session.LastActivity = time.Now()
// Save to persistence if configured
- sts.saveSessions()
+ thinking.saveSessions()
// Determine status
status := "thinking"
@@ -306,11 +275,11 @@ func (sts *Server) HandleSequentialThinking(req mcp.CallToolRequest) (mcp.CallTo
// If this is the final thought, try to extract a solution
if status == "completed" {
- response.Solution = sts.extractSolution(params.Thought)
+ response.Solution = thinking.extractSolution(params.Thought)
}
// Format the result with session context
- resultText := sts.formatThinkingResultWithSession(response, currentThought, session, activeBranch)
+ resultText := thinking.formatThinkingResultWithSession(response, currentThought, session, activeBranch)
return mcp.NewToolResult(mcp.NewTextContent(resultText)), nil
}
@@ -324,16 +293,16 @@ type PersistentData struct {
}
// loadSessions loads sessions from persistence file
-func (sts *Server) loadSessions() error {
- if sts.persistFile == "" {
+func (thinking *ThinkingOperations) loadSessions() error {
+ if thinking.persistFile == "" {
return nil
}
- if _, err := os.Stat(sts.persistFile); os.IsNotExist(err) {
+ if _, err := os.Stat(thinking.persistFile); os.IsNotExist(err) {
return nil // File doesn't exist, start fresh
}
- data, err := os.ReadFile(sts.persistFile)
+ data, err := os.ReadFile(thinking.persistFile)
if err != nil {
return err
}
@@ -347,29 +316,29 @@ func (sts *Server) loadSessions() error {
return err
}
- sts.sessions = persistentData.Sessions
- sts.branches = persistentData.Branches
+ thinking.sessions = persistentData.Sessions
+ thinking.branches = persistentData.Branches
// Initialize maps if nil
- if sts.sessions == nil {
- sts.sessions = make(map[string]*ThinkingSession)
+ if thinking.sessions == nil {
+ thinking.sessions = make(map[string]*ThinkingSession)
}
- if sts.branches == nil {
- sts.branches = make(map[string]*Branch)
+ if thinking.branches == nil {
+ thinking.branches = make(map[string]*Branch)
}
return nil
}
// saveSessions saves sessions to persistence file
-func (sts *Server) saveSessions() error {
- if sts.persistFile == "" {
+func (thinking *ThinkingOperations) saveSessions() error {
+ if thinking.persistFile == "" {
return nil
}
persistentData := PersistentData{
- Sessions: sts.sessions,
- Branches: sts.branches,
+ Sessions: thinking.sessions,
+ Branches: thinking.branches,
}
data, err := json.MarshalIndent(persistentData, "", " ")
@@ -377,7 +346,7 @@ func (sts *Server) saveSessions() error {
return err
}
- return os.WriteFile(sts.persistFile, data, 0644)
+ return os.WriteFile(thinking.persistFile, data, 0644)
}
// ThinkingParameters holds parsed parameters for thinking operations
@@ -395,7 +364,7 @@ type ThinkingParameters struct {
}
// parseThinkingParameters parses and validates input parameters
-func (sts *Server) parseThinkingParameters(args map[string]interface{}) (*ThinkingParameters, error) {
+func (thinking *ThinkingOperations) parseThinkingParameters(args map[string]interface{}) (*ThinkingParameters, error) {
params := &ThinkingParameters{}
// Required parameters
@@ -488,12 +457,12 @@ func (sts *Server) parseThinkingParameters(args map[string]interface{}) (*Thinki
}
// getOrCreateSession gets existing session or creates new one
-func (sts *Server) getOrCreateSession(sessionID string) *ThinkingSession {
+func (thinking *ThinkingOperations) getOrCreateSession(sessionID string) *ThinkingSession {
if sessionID == "" {
- sessionID = sts.generateSessionID()
+ sessionID = thinking.generateSessionID()
}
- if session, exists := sts.sessions[sessionID]; exists {
+ if session, exists := thinking.sessions[sessionID]; exists {
return session
}
@@ -508,13 +477,13 @@ func (sts *Server) getOrCreateSession(sessionID string) *ThinkingSession {
ActiveBranches: make([]string, 0),
}
- sts.sessions[sessionID] = session
+ thinking.sessions[sessionID] = session
return session
}
// getOrCreateBranch gets existing branch or creates new one
-func (sts *Server) getOrCreateBranch(sessionID, branchID string, fromThought int) *Branch {
- if branch, exists := sts.branches[branchID]; exists {
+func (thinking *ThinkingOperations) getOrCreateBranch(sessionID, branchID string, fromThought int) *Branch {
+ if branch, exists := thinking.branches[branchID]; exists {
return branch
}
@@ -527,10 +496,10 @@ func (sts *Server) getOrCreateBranch(sessionID, branchID string, fromThought int
LastActivity: time.Now(),
}
- sts.branches[branchID] = branch
+ thinking.branches[branchID] = branch
// Add to session's active branches
- if session, exists := sts.sessions[sessionID]; exists {
+ if session, exists := thinking.sessions[sessionID]; exists {
session.ActiveBranches = append(session.ActiveBranches, branchID)
}
@@ -538,12 +507,12 @@ func (sts *Server) getOrCreateBranch(sessionID, branchID string, fromThought int
}
// generateSessionID generates a unique session ID
-func (sts *Server) generateSessionID() string {
+func (thinking *ThinkingOperations) generateSessionID() string {
return fmt.Sprintf("session_%d", time.Now().UnixNano())
}
// formatThinkingResultWithSession formats result with session context
-func (sts *Server) formatThinkingResultWithSession(response ThinkingResponse, thought Thought, session *ThinkingSession, branch *Branch) string {
+func (thinking *ThinkingOperations) formatThinkingResultWithSession(response ThinkingResponse, thought Thought, session *ThinkingSession, branch *Branch) string {
var result strings.Builder
// Header with session info
@@ -585,7 +554,7 @@ func (sts *Server) formatThinkingResultWithSession(response ThinkingResponse, th
result.WriteString(response.Thought + "\n\n")
// Progress indicator
- progressBar := sts.createProgressBar(response.ThoughtNumber, response.TotalThoughts)
+ progressBar := thinking.createProgressBar(response.ThoughtNumber, response.TotalThoughts)
result.WriteString("๐ Progress: " + progressBar + "\n\n")
// Status
@@ -617,7 +586,7 @@ func (sts *Server) formatThinkingResultWithSession(response ThinkingResponse, th
return result.String()
}
-func (sts *Server) extractSolution(finalThought string) string {
+func (thinking *ThinkingOperations) extractSolution(finalThought string) string {
// Simple heuristic to extract a solution from the final thought
content := strings.ToLower(finalThought)
@@ -670,7 +639,7 @@ func (sts *Server) extractSolution(finalThought string) string {
return "Solution extracted from final thought"
}
-func (sts *Server) formatThinkingResult(response ThinkingResponse, thought Thought, contextInfo []string) string {
+func (thinking *ThinkingOperations) formatThinkingResult(response ThinkingResponse, thought Thought, contextInfo []string) string {
var result strings.Builder
// Header
@@ -688,7 +657,7 @@ func (sts *Server) formatThinkingResult(response ThinkingResponse, thought Thoug
result.WriteString(response.Thought + "\n\n")
// Progress indicator
- progressBar := sts.createProgressBar(response.ThoughtNumber, response.TotalThoughts)
+ progressBar := thinking.createProgressBar(response.ThoughtNumber, response.TotalThoughts)
result.WriteString("๐ Progress: " + progressBar + "\n\n")
// Status
@@ -720,7 +689,7 @@ func (sts *Server) formatThinkingResult(response ThinkingResponse, thought Thoug
return result.String()
}
-func (sts *Server) createProgressBar(current, total int) string {
+func (thinking *ThinkingOperations) createProgressBar(current, total int) string {
if total <= 0 {
return "[โโโโโโโโโโโโโโโโโโโโ] 100%"
}
@@ -748,16 +717,16 @@ func (sts *Server) createProgressBar(current, total int) string {
// New tool handlers for session management
-func (sts *Server) HandleGetSessionHistory(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (thinking *ThinkingOperations) handleGetSessionHistory(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
sessionID, ok := req.Arguments["sessionId"].(string)
if !ok {
return mcp.NewToolError("sessionId parameter is required"), nil
}
- sts.mu.RLock()
- defer sts.mu.RUnlock()
+ thinking.mu.RLock()
+ defer thinking.mu.RUnlock()
- session, exists := sts.sessions[sessionID]
+ session, exists := thinking.sessions[sessionID]
if !exists {
return mcp.NewToolError(fmt.Sprintf("Session %s not found", sessionID)), nil
}
@@ -776,12 +745,12 @@ func (sts *Server) HandleGetSessionHistory(req mcp.CallToolRequest) (mcp.CallToo
return mcp.NewToolResult(mcp.NewTextContent(string(jsonData))), nil
}
-func (sts *Server) HandleListSessions(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- sts.mu.RLock()
- defer sts.mu.RUnlock()
+func (thinking *ThinkingOperations) handleListSessions(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ thinking.mu.RLock()
+ defer thinking.mu.RUnlock()
- sessions := make([]map[string]interface{}, 0, len(sts.sessions))
- for _, session := range sts.sessions {
+ sessions := make([]map[string]interface{}, 0, len(thinking.sessions))
+ for _, session := range thinking.sessions {
sessionInfo := map[string]interface{}{
"sessionId": session.ID,
"status": session.Status,
@@ -802,16 +771,16 @@ func (sts *Server) HandleListSessions(req mcp.CallToolRequest) (mcp.CallToolResu
return mcp.NewToolResult(mcp.NewTextContent(string(jsonData))), nil
}
-func (sts *Server) HandleGetBranchHistory(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (thinking *ThinkingOperations) handleGetBranchHistory(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
branchID, ok := req.Arguments["branchId"].(string)
if !ok {
return mcp.NewToolError("branchId parameter is required"), nil
}
- sts.mu.RLock()
- defer sts.mu.RUnlock()
+ thinking.mu.RLock()
+ defer thinking.mu.RUnlock()
- branch, exists := sts.branches[branchID]
+ branch, exists := thinking.branches[branchID]
if !exists {
return mcp.NewToolError(fmt.Sprintf("Branch %s not found", branchID)), nil
}
@@ -830,30 +799,30 @@ func (sts *Server) HandleGetBranchHistory(req mcp.CallToolRequest) (mcp.CallTool
return mcp.NewToolResult(mcp.NewTextContent(string(jsonData))), nil
}
-func (sts *Server) HandleClearSession(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+func (thinking *ThinkingOperations) handleClearSession(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
sessionID, ok := req.Arguments["sessionId"].(string)
if !ok {
return mcp.NewToolError("sessionId parameter is required"), nil
}
- sts.mu.Lock()
- defer sts.mu.Unlock()
+ thinking.mu.Lock()
+ defer thinking.mu.Unlock()
- session, exists := sts.sessions[sessionID]
+ session, exists := thinking.sessions[sessionID]
if !exists {
return mcp.NewToolError(fmt.Sprintf("Session %s not found", sessionID)), nil
}
// Remove associated branches
for _, branchID := range session.ActiveBranches {
- delete(sts.branches, branchID)
+ delete(thinking.branches, branchID)
}
// Remove the session
- delete(sts.sessions, sessionID)
+ delete(thinking.sessions, sessionID)
// Save to persistence if configured
- sts.saveSessions()
+ thinking.saveSessions()
result := map[string]interface{}{
"message": fmt.Sprintf("Session %s and %d branches cleared", sessionID, len(session.ActiveBranches)),
pkg/time/server.go
@@ -10,12 +10,6 @@ import (
"github.com/xlgmokha/mcp/pkg/mcp"
)
-// Server implements the Time MCP server
-type Server struct {
- *mcp.Server
- localTimezone string
-}
-
// TimeResult represents the result of a time operation
type TimeResult struct {
Timezone string `json:"timezone"`
@@ -30,139 +24,105 @@ type TimeConversionResult struct {
TimeDifference string `json:"time_difference"`
}
-// New creates a new Time MCP server
-func New() *Server {
- server := mcp.NewServer("mcp-time", "1.0.0", []mcp.Tool{}, []mcp.Resource{}, []mcp.Root{})
-
- // Get local timezone
- localTZ := getLocalTimezone()
-
- timeServer := &Server{
- Server: server,
- localTimezone: localTZ,
- }
-
- // Register all time tools
- timeServer.registerTools()
-
- return timeServer
+// TimeOperations provides time operations and timezone handling
+type TimeOperations struct {
+ localTimezone string
}
-// registerTools registers all Time tools with the server
-func (ts *Server) registerTools() {
- // Get all tool definitions from ListTools method
- tools := ts.ListTools()
-
- // Register each tool with its proper definition
- for _, tool := range tools {
- var handler mcp.ToolHandler
- switch tool.Name {
- case "get_current_time":
- handler = ts.HandleGetCurrentTime
- case "convert_time":
- handler = ts.HandleConvertTime
- default:
- continue
- }
- ts.RegisterToolWithDefinition(tool, handler)
+// NewTimeOperations creates a new TimeOperations helper
+func NewTimeOperations() *TimeOperations {
+ return &TimeOperations{
+ localTimezone: getLocalTimezone(),
}
}
-// ListTools returns all available Time tools
-func (ts *Server) ListTools() []mcp.Tool {
- return []mcp.Tool{
- {
- Name: "get_current_time",
- Description: "Get current time in a specific timezone",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "timezone": map[string]interface{}{
- "type": "string",
- "description": fmt.Sprintf("IANA timezone name (e.g., 'America/New_York', 'Europe/London'). Use '%s' as local timezone if no timezone provided by the user.", ts.localTimezone),
- },
- },
- "required": []string{"timezone"},
- },
- },
- {
- Name: "convert_time",
- Description: "Convert time between timezones",
- InputSchema: map[string]interface{}{
- "type": "object",
- "properties": map[string]interface{}{
- "source_timezone": map[string]interface{}{
- "type": "string",
- "description": fmt.Sprintf("Source IANA timezone name (e.g., 'America/New_York', 'Europe/London'). Use '%s' as local timezone if no source timezone provided by the user.", ts.localTimezone),
- },
- "time": map[string]interface{}{
- "type": "string",
- "description": "Time to convert in 24-hour format (HH:MM)",
- },
- "target_timezone": map[string]interface{}{
- "type": "string",
- "description": fmt.Sprintf("Target IANA timezone name (e.g., 'Asia/Tokyo', 'America/San_Francisco'). Use '%s' as local timezone if no target timezone provided by the user.", ts.localTimezone),
- },
- },
- "required": []string{"source_timezone", "time", "target_timezone"},
+// New creates a new Time MCP server
+func New() *mcp.Server {
+ timeOps := NewTimeOperations()
+ builder := mcp.NewServerBuilder("mcp-time", "1.0.0")
+
+ // Add get_current_time tool
+ builder.AddTool(mcp.NewTool("get_current_time", "Get current time in a specific timezone", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "timezone": map[string]interface{}{
+ "type": "string",
+ "description": fmt.Sprintf("IANA timezone name (e.g., 'America/New_York', 'Europe/London'). Use '%s' as local timezone if no timezone provided by the user.", timeOps.localTimezone),
},
},
- }
-}
-
-// Tool handlers
+ "required": []string{"timezone"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ timezone, ok := req.Arguments["timezone"].(string)
+ if !ok {
+ return mcp.NewToolError("timezone is required"), nil
+ }
-func (ts *Server) HandleGetCurrentTime(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- timezone, ok := req.Arguments["timezone"].(string)
- if !ok {
- return mcp.NewToolError("timezone is required"), nil
- }
+ result, err := timeOps.getCurrentTime(timezone)
+ if err != nil {
+ return mcp.NewToolError(err.Error()), nil
+ }
- result, err := ts.getCurrentTime(timezone)
- if err != nil {
- return mcp.NewToolError(err.Error()), nil
- }
+ jsonResult, err := json.MarshalIndent(result, "", " ")
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to marshal result: %v", err)), nil
+ }
- jsonResult, err := json.MarshalIndent(result, "", " ")
- if err != nil {
- return mcp.NewToolError(fmt.Sprintf("Failed to marshal result: %v", err)), nil
- }
+ return mcp.NewToolResult(mcp.NewTextContent(string(jsonResult))), nil
+ }))
- return mcp.NewToolResult(mcp.NewTextContent(string(jsonResult))), nil
-}
+ // Add convert_time tool
+ builder.AddTool(mcp.NewTool("convert_time", "Convert time between timezones", map[string]interface{}{
+ "type": "object",
+ "properties": map[string]interface{}{
+ "source_timezone": map[string]interface{}{
+ "type": "string",
+ "description": fmt.Sprintf("Source IANA timezone name (e.g., 'America/New_York', 'Europe/London'). Use '%s' as local timezone if no source timezone provided by the user.", timeOps.localTimezone),
+ },
+ "time": map[string]interface{}{
+ "type": "string",
+ "description": "Time to convert in 24-hour format (HH:MM)",
+ },
+ "target_timezone": map[string]interface{}{
+ "type": "string",
+ "description": fmt.Sprintf("Target IANA timezone name (e.g., 'Asia/Tokyo', 'America/San_Francisco'). Use '%s' as local timezone if no target timezone provided by the user.", timeOps.localTimezone),
+ },
+ },
+ "required": []string{"source_timezone", "time", "target_timezone"},
+ }, func(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
+ sourceTimezone, ok := req.Arguments["source_timezone"].(string)
+ if !ok {
+ return mcp.NewToolError("source_timezone is required"), nil
+ }
-func (ts *Server) HandleConvertTime(req mcp.CallToolRequest) (mcp.CallToolResult, error) {
- sourceTimezone, ok := req.Arguments["source_timezone"].(string)
- if !ok {
- return mcp.NewToolError("source_timezone is required"), nil
- }
+ timeStr, ok := req.Arguments["time"].(string)
+ if !ok {
+ return mcp.NewToolError("time is required"), nil
+ }
- timeStr, ok := req.Arguments["time"].(string)
- if !ok {
- return mcp.NewToolError("time is required"), nil
- }
+ targetTimezone, ok := req.Arguments["target_timezone"].(string)
+ if !ok {
+ return mcp.NewToolError("target_timezone is required"), nil
+ }
- targetTimezone, ok := req.Arguments["target_timezone"].(string)
- if !ok {
- return mcp.NewToolError("target_timezone is required"), nil
- }
+ result, err := timeOps.convertTime(sourceTimezone, timeStr, targetTimezone)
+ if err != nil {
+ return mcp.NewToolError(err.Error()), nil
+ }
- result, err := ts.convertTime(sourceTimezone, timeStr, targetTimezone)
- if err != nil {
- return mcp.NewToolError(err.Error()), nil
- }
+ jsonResult, err := json.MarshalIndent(result, "", " ")
+ if err != nil {
+ return mcp.NewToolError(fmt.Sprintf("Failed to marshal result: %v", err)), nil
+ }
- jsonResult, err := json.MarshalIndent(result, "", " ")
- if err != nil {
- return mcp.NewToolError(fmt.Sprintf("Failed to marshal result: %v", err)), nil
- }
+ return mcp.NewToolResult(mcp.NewTextContent(string(jsonResult))), nil
+ }))
- return mcp.NewToolResult(mcp.NewTextContent(string(jsonResult))), nil
+ return builder.Build()
}
-// Helper methods
+// Helper methods for TimeOperations
-func (ts *Server) getCurrentTime(timezone string) (*TimeResult, error) {
+func (timeOps *TimeOperations) getCurrentTime(timezone string) (*TimeResult, error) {
loc, err := time.LoadLocation(timezone)
if err != nil {
return nil, fmt.Errorf("Invalid timezone: %v", err)
@@ -177,7 +137,7 @@ func (ts *Server) getCurrentTime(timezone string) (*TimeResult, error) {
}, nil
}
-func (ts *Server) convertTime(sourceTimezone, timeStr, targetTimezone string) (*TimeConversionResult, error) {
+func (timeOps *TimeOperations) convertTime(sourceTimezone, timeStr, targetTimezone string) (*TimeConversionResult, error) {
sourceLoc, err := time.LoadLocation(sourceTimezone)
if err != nil {
return nil, fmt.Errorf("Invalid source timezone: %v", err)