# 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 | [](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 |
```