#
tokens: 2653/50000 5/5 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── Dockerfile
├── go.mod
├── go.sum
├── main.go
├── README.md
└── smithery.yaml
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | prolog_mcp
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Prolog MCP
 2 | [![smithery badge](https://smithery.ai/badge/@snoglobe/prolog_mcp)](https://smithery.ai/server/@snoglobe/prolog_mcp)
 3 | 
 4 | An MCP that provides tools for executing Prolog, querying it, and searching existing predicates.
 5 | 
 6 | # To install
 7 | 
 8 | ### Installing via Smithery
 9 | 
10 | To install prolog_mcp for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@snoglobe/prolog_mcp):
11 | 
12 | ```bash
13 | npx -y @smithery/cli install @snoglobe/prolog_mcp --client claude
14 | ```
15 | 
16 | ### Manual Installation
17 | Uhhhh build it to an executable and add the full path of the executable to your MCP config with no args
18 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     properties: {}
 9 |   commandFunction:
10 |     # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
11 |     |-
12 |     (config) => ({ command: './mcp' })
13 |   exampleConfig: {}
14 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | FROM golang:1.24-alpine AS builder
 3 | 
 4 | WORKDIR /app
 5 | 
 6 | # Install git if needed
 7 | RUN apk add --no-cache git
 8 | 
 9 | # Copy go mod and sum files
10 | COPY go.mod go.sum ./
11 | RUN go mod download
12 | 
13 | # Copy the source code
14 | COPY . .
15 | 
16 | # Build the binary
17 | RUN CGO_ENABLED=0 go build -o mcp main.go
18 | 
19 | FROM alpine:latest
20 | 
21 | WORKDIR /app
22 | 
23 | # Copy the binary from the builder
24 | COPY --from=builder /app/mcp ./mcp
25 | 
26 | ENTRYPOINT ["./mcp"]
27 | 
```

--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------

```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"errors"
  6 | 	"fmt"
  7 | 	"reflect"
  8 | 	"time"
  9 | 	"unsafe"
 10 | 
 11 | 	"github.com/ichiban/prolog"
 12 | 	"github.com/mark3labs/mcp-go/mcp"
 13 | 	"github.com/mark3labs/mcp-go/server"
 14 | )
 15 | 
 16 | func main() {
 17 | 	// Create a new MCP server
 18 | 	s := server.NewMCPServer(
 19 | 		"Prolog MCP",
 20 | 		"1.0.0",
 21 | 		server.WithResourceCapabilities(true, true),
 22 | 		server.WithLogging(),
 23 | 	)
 24 | 
 25 | 	p := prolog.New(nil, nil)
 26 | 	prologCtx := context.Background()
 27 | 
 28 | 	query := mcp.NewTool("query", mcp.WithDescription("Query the prolog engine"),
 29 | 		mcp.WithString("query", mcp.Required(), mcp.Description("The query to execute")),
 30 | 	)
 31 | 
 32 | 	exec := mcp.NewTool("exec", mcp.WithDescription("Execute a prolog program"),
 33 | 		mcp.WithString("program", mcp.Required(), mcp.Description("The prolog program to execute")),
 34 | 	)
 35 | 
 36 | 	discover := mcp.NewTool("discover", mcp.WithDescription("Shows the available predicates in the prolog engine"))
 37 | 
 38 | 	s.AddTool(query, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 39 | 		query := request.Params.Arguments["query"].(string)
 40 | 
 41 | 		// Create a context with a 15-second timeout derived from the handler's context
 42 | 		queryCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
 43 | 		defer cancel() // Ensure the cancel function is called to release resources
 44 | 
 45 | 		// Use the time-limited context for the query
 46 | 		solutions, err := p.QueryContext(queryCtx, query) // Pass queryCtx here
 47 | 		if err != nil {
 48 | 			// Check specifically for context deadline exceeded
 49 | 			if errors.Is(err, context.DeadlineExceeded) {
 50 | 				return nil, fmt.Errorf("query timed out after 15 seconds: %w", err)
 51 | 			}
 52 | 			return nil, fmt.Errorf("query error: %w", err)
 53 | 		}
 54 | 		defer solutions.Close() // Ensure resources are released
 55 | 
 56 | 		var allSolutions []map[string]any // Slice to hold all solution maps
 57 | 
 58 | 		// Iterate through all possible solutions
 59 | 		for solutions.Next() {
 60 | 			solutionMap := make(map[string]any)
 61 | 			// Scan the *current* solution's bindings into the map
 62 | 			if err := solutions.Scan(solutionMap); err != nil {
 63 | 				// Handle potential scan errors (though less common if Next() succeeded)
 64 | 				return nil, fmt.Errorf("error scanning solution: %w", err)
 65 | 			}
 66 | 			allSolutions = append(allSolutions, solutionMap) // Add the map for this solution
 67 | 		}
 68 | 
 69 | 		// Check for errors *after* iteration (e.g., resource limits exceeded, internal errors, timeout)
 70 | 		if err := solutions.Err(); err != nil {
 71 | 			// Check specifically for context deadline exceeded during iteration
 72 | 			if errors.Is(err, context.DeadlineExceeded) {
 73 | 				return nil, fmt.Errorf("query iteration timed out after 15 seconds: %w", err)
 74 | 			}
 75 | 			return nil, fmt.Errorf("error during query iteration: %w", err)
 76 | 		}
 77 | 
 78 | 		// Format the result (using fmt.Sprintf for consistency)
 79 | 		// If no solutions were found, allSolutions will be an empty slice: []
 80 | 		return mcp.NewToolResultText(fmt.Sprintf("%v", allSolutions)), nil
 81 | 	})
 82 | 
 83 | 	s.AddTool(exec, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 84 | 		program := request.Params.Arguments["program"].(string)
 85 | 		err := p.ExecContext(prologCtx, program)
 86 | 		if err != nil {
 87 | 			return nil, err
 88 | 		}
 89 | 		return mcp.NewToolResultText("Program executed successfully"), nil
 90 | 	})
 91 | 
 92 | 	s.AddTool(discover, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 93 | 		predicates := new([]string)
 94 | 
 95 | 		v := reflect.ValueOf(p).Elem()
 96 | 
 97 | 		proceduresField := v.FieldByName("procedures")
 98 | 
 99 | 		var accessibleProcedures reflect.Value
100 | 		var baseValue reflect.Value
101 | 
102 | 		if proceduresField.IsValid() {
103 | 			baseValue = v
104 | 		} else {
105 | 			vmField := v.FieldByName("VM")
106 | 			if !vmField.IsValid() {
107 | 				return nil, fmt.Errorf("could not find 'procedures' field (tried promotion and explicit 'VM' embed)")
108 | 			}
109 | 			proceduresField = vmField.FieldByName("procedures")
110 | 			if !proceduresField.IsValid() {
111 | 				return nil, fmt.Errorf("found embedded 'VM' field, but not 'procedures' field within it")
112 | 			}
113 | 			baseValue = vmField
114 | 		}
115 | 
116 | 		if !baseValue.CanAddr() {
117 | 			return nil, fmt.Errorf("internal error: base value for field is not addressable")
118 | 		}
119 | 
120 | 		// Get the field description (StructField) from the base struct's type to find its offset.
121 | 		structFieldDesc, found := baseValue.Type().FieldByName("procedures")
122 | 		if !found {
123 | 			// This should theoretically not happen based on prior checks finding the Value
124 | 			return nil, fmt.Errorf("internal error: could not get StructField description for 'procedures' in type %v", baseValue.Type())
125 | 		}
126 | 
127 | 		// Calculate the pointer using the Offset from the StructField description.
128 | 		fieldPtr := unsafe.Pointer(baseValue.UnsafeAddr() + structFieldDesc.Offset)
129 | 
130 | 		// Create the accessible value using the Type from the original field Value.
131 | 		accessibleProcedures = reflect.NewAt(proceduresField.Type(), fieldPtr).Elem()
132 | 
133 | 		if accessibleProcedures.Kind() == reflect.Map {
134 | 			iter := accessibleProcedures.MapRange()
135 | 			for iter.Next() {
136 | 				k := iter.Key()
137 | 
138 | 				pkMethod := k.MethodByName("String")
139 | 				var procString string
140 | 				if pkMethod.IsValid() {
141 | 					results := pkMethod.Call(nil)
142 | 					if len(results) > 0 && results[0].Kind() == reflect.String {
143 | 						procString = results[0].String()
144 | 					} else {
145 | 						procString = fmt.Sprintf("error<key=%v, unexpected_result=%v>", k, results)
146 | 					}
147 | 				} else {
148 | 					procString = fmt.Sprintf("error<key=%v, method_not_found>", k)
149 | 				}
150 | 				*predicates = append(*predicates, procString)
151 | 			}
152 | 		} else {
153 | 			return nil, fmt.Errorf("procedures field is not a map, kind: %v", accessibleProcedures.Kind())
154 | 		}
155 | 		resultString := fmt.Sprintf("%v", *predicates)
156 | 		return mcp.NewToolResultText(resultString), nil
157 | 	})
158 | 
159 | 	// Start the stdio server
160 | 	if err := server.ServeStdio(s); err != nil {
161 | 		fmt.Printf("Server error: %v\n", err)
162 | 	}
163 | }
164 | 
```