# Directory Structure
```
├── .github
│   └── workflows
│       └── go-test.yml
├── .gitignore
├── cmd
│   ├── main.go
│   └── temporal-mcp
│       ├── hash_args.go
│       ├── main_test.go
│       └── main.go
├── config.sample.yml
├── docs
│   ├── temporal.md
│   ├── VERSION_0-project.md
│   └── VERSION_0.md
├── examples
│   ├── generate_claude_config.sh
│   └── README.md
├── go.mod
├── go.sum
├── internal
│   ├── config
│   │   ├── config_test.go
│   │   └── config.go
│   ├── sanitize_history_event
│   │   ├── sanitize_history_event_test.go
│   │   ├── sanitize_history_event.go
│   │   └── test_data
│   │       ├── foo_original.jsonl
│   │       └── foo_sanitized.jsonl
│   ├── temporal
│   │   ├── client_test.go
│   │   ├── client.go
│   │   └── logger.go
│   └── tool
│       ├── definition.go
│       ├── registry_test.go
│       └── registry.go
├── Makefile
└── README.md
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
 1 | # Binaries for programs and plugins
 2 | *.exe
 3 | *.exe~
 4 | *.dll
 5 | *.so
 6 | *.dylib
 7 | 
 8 | # Test binary, built with `go test -c`
 9 | *.test
10 | 
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 | 
14 | # Dependency directories (remove the comment below to include it)
15 | vendor/
16 | 
17 | # Go workspace file
18 | go.work
19 | 
20 | # IDE specific files
21 | .idea/
22 | .vscode/
23 | *.swp
24 | *.swo
25 | 
26 | # OS specific files
27 | .DS_Store
28 | .DS_Store?
29 | ._*
30 | .Spotlight-V100
31 | .Trashes
32 | ehthumbs.db
33 | Thumbs.db
34 | 
35 | # Build output
36 | bin/
37 | dist/
38 | build/
39 | 
40 | # Environment variables
41 | .env
42 | .env.local
43 | 
44 | # Configuration files
45 | config.yml
46 | examples/claude_config.json
47 | 
48 | # Database files
49 | *.db
```
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
```markdown
 1 | # Temporal MCP Examples
 2 | 
 3 | This directory contains examples and configuration for the Temporal Model Context Protocol (MCP) server that exposes Temporal workflows as tools for AI assistants.
 4 | 
 5 | ## What is MCP?
 6 | 
 7 | The Model Context Protocol (MCP) is a protocol that allows AI models like Claude to interact with external tools and services. It provides a standardized way for AI models to access functionality outside of their training data.
 8 | 
 9 | ## What is Temporal?
10 | 
11 | [Temporal](https://temporal.io/) is a workflow orchestration platform that simplifies the development of reliable applications. The Temporal MCP server allows Claude to execute and interact with Temporal workflows, enabling complex task automation, data processing, and service orchestration.
12 | 
13 | ## Features
14 | 
15 | The Temporal MCP server provides access to workflows configured in `config.yml`, such as:
16 | 
17 | 1. **Dynamic workflow execution** - Run any workflow defined in the configuration
18 | 2. **Cached results** - Optionally cache workflow results for improved performance
19 | 3. **Task queue management** - Configure specific or default task queues for workflow execution
20 | 
21 | ## Using with Claude Desktop
22 | 
23 | ### Automatic Configuration (Recommended)
24 | 
25 | The easiest way to configure Claude Desktop is to use the provided script:
26 | 
27 | 1. Build the MCP server using the Makefile from the root directory:
28 |    ```bash
29 |    cd .. && make build
30 |    ```
31 | 
32 | 2. Run the configuration script from the examples directory:
33 |    ```bash
34 |    ./generate_claude_config.sh
35 |    ```
36 |    This will:
37 |    - Generate a `claude_config.json` file with correct paths for your system
38 |    - Add the file to .gitignore to prevent committing personal paths
39 |    - Show instructions for deploying the config file
40 | 
41 | 3. Copy the generated config to Claude's configuration directory:
42 |    ```bash
43 |    cp claude_config.json ~/Library/Application\ Support/Claude/claude_desktop_config.json
44 |    ```
45 | 
46 | 4. Restart Claude Desktop
47 | 
48 | ### Manual Configuration
49 | 
50 | Alternatively, you can manually create a configuration file at `~/Library/Application Support/Claude/claude_desktop_config.json` with the following content:
51 |    ```json
52 |    {
53 |      "mcpServers": {
54 |        "temporal-mcp": {
55 |          "command": "/full/path/to/your/bin/temporal-mcp",
56 |          "args": ["--config", "/full/path/to/your/config.yml"],
57 |          "env": {}
58 |        }
59 |      }
60 |    }
61 |    ```
62 |    
63 |    Remember to replace the paths with the actual full paths to your binaries and config file.
64 | 
65 | 4. When chatting with Claude, you can ask it to use the Pig Latin conversion tools.
66 | 
67 | ## Example Prompts for Claude
68 | 
69 | 
70 | 
71 | ### Temporal MCP
72 | 
73 | Once connected to the Temporal MCP server, you can ask Claude things like:
74 | 
75 | - "Can you run the GreetingWorkflow with my name as a parameter?"
76 | - "Please execute the DataProcessingWorkflow with the following parameters..."
77 | - "Clear the cache for all workflows"
78 | - "Run the AnalyticsWorkflow and show me the results"
79 | 
80 | 
81 | ## How It Works
82 | 
83 | The Temporal MCP server also uses the [mcp-golang](https://github.com/metoro-io/mcp-golang) library but connects to a Temporal service to execute workflows. When Claude needs to run a workflow:
84 | 
85 | 1. It recognizes the need to execute a Temporal workflow
86 | 2. It calls the appropriate workflow tool with the required parameters
87 | 3. The MCP server executes the workflow on the Temporal service
88 | 4. The workflow result is returned to Claude
89 | 5. Claude presents the result to the user
90 | 
91 | The Temporal MCP server also supports result caching to improve performance for repetitive workflow executions.
92 | 
93 | 
94 | 
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
  1 | # ⏰🧠 Temporal-MCP Server
  2 | [](https://deepwiki.com/Mocksi/temporal-mcp)
  3 | [](https://github.com/Mocksi/temporal-mcp/actions/workflows/go-test.yml)
  4 | 
  5 | 
  6 | Temporal MCP is an MCP server that bridges AI assistants (like Claude) and Temporal workflows. It turns complex backend orchestration into simple, chat-driven commands. Imagine triggering stateful processes without writing a line of glue code. Temporal-MCP makes that possible.
  7 | 
  8 | ## Why Temporal MCP
  9 | 
 10 | - **Supercharged AI** — AI assistants gain reliable, long-running workflow superpowers
 11 | - **Conversational Orchestration** — Trigger, monitor, and manage workflows through natural language
 12 | - **Enterprise-Ready** — Leverage Temporal's retries, timeouts, and persistence—exposed in plain text
 13 | 
 14 | ## ✨ Key Features
 15 | 
 16 | - **🔍 Automatic Discovery** — Explore available workflows and see rich metadata
 17 | - **🏃♂️ Seamless Execution** — Kick off complex processes with a single chat message
 18 | - **📊 Real-time Monitoring** — Follow progress, check status, and get live updates
 19 | - **⚡ Performance Optimization** — Smart caching for instant answers
 20 | - **🧠 AI-Friendly Descriptions** — Purpose fields written for both humans and machines
 21 | 
 22 | ## 🏁 Getting Started
 23 | 
 24 | ### Prerequisites
 25 | 
 26 | - **Go 1.21+** — For building and running the MCP server
 27 | - **Temporal Server** — Running locally or remotely (see [Temporal docs](https://docs.temporal.io/docs/server/quick-install/))
 28 | 
 29 | ### Quick Install
 30 | 
 31 | 1. Run your Temporal server and workers
 32 | In this example, we'll use the [Temporal Money Transfer Demo](https://github.com/temporal-sa/money-transfer-demo/tree/main).
 33 | 
 34 | 
 35 | #### MCP Setup
 36 | Get Claude (or similar MCP-enabled AI assistant) talking to your workflows in 5 easy steps:
 37 | 
 38 | 2. **Build the server**
 39 | ```bash
 40 | git clone https://github.com/Mocksi/temporal-mcp.git
 41 | cd temporal-mcp
 42 | make build
 43 | ```
 44 | 
 45 | 2. **Define your workflows** in `config.yml`
 46 | The sample configuration (`config.sample.yml`) is designed to work with the [Temporal Money Transfer Demo](https://github.com/temporal-sa/money-transfer-demo/tree/main):
 47 | 
 48 | ```yaml
 49 | workflows:
 50 |   AccountTransferWorkflow:
 51 |     purpose: "Transfers money between accounts with validation and notification. Handles the happy path scenario where everything works as expected."
 52 |     input:
 53 |       type: "TransferInput"
 54 |       fields:
 55 |         - from_account: "Source account ID"
 56 |         - to_account: "Destination account ID"
 57 |         - amount: "Amount to transfer"
 58 |     output:
 59 |       type: "TransferOutput"
 60 |       description: "Transfer confirmation with charge ID"
 61 |     taskQueue: "account-transfer-queue"
 62 | 
 63 |   AccountTransferWorkflowScenarios:
 64 |     purpose: "Extended account transfer workflow with various scenarios including human approval, recoverable failures, and advanced visibility features."
 65 |     input:
 66 |       type: "TransferInput"
 67 |       fields:
 68 |         - from_account: "Source account ID"
 69 |         - to_account: "Destination account ID"
 70 |         - amount: "Amount to transfer"
 71 |         - scenario_type: "Type of scenario to execute (human_approval, recoverable_failure, advanced_visibility)"
 72 |     output:
 73 |       type: "TransferOutput"
 74 |       description: "Transfer confirmation with charge ID"
 75 |     taskQueue: "account-transfer-queue"
 76 | ```
 77 | 
 78 | 3. **Generate Claude's configuration**
 79 | ```bash
 80 | cd examples
 81 | ./generate_claude_config.sh
 82 | ```
 83 | 
 84 | 4. **Install the configuration**
 85 | ```bash
 86 | cp examples/claude_config.json ~/Library/Application\ Support/Claude/claude_desktop_config.json
 87 | ```
 88 | 
 89 | 5. **Start Claude** with this configuration
 90 | 
 91 | ### Conversing with Your Workflows
 92 | 
 93 | Now for the magic part! Talk to your workflows through Claude using natural language:
 94 | 
 95 | > 💬 "Claude, can you transfer $100 from account ABC123 to account XYZ789?"
 96 | 
 97 | > 💬 "What transfer scenarios are available to test?"
 98 | 
 99 | > 💬 "Execute a transfer that requires human approval for $500 between accounts ABC123 and XYZ789"
100 | 
101 | > 💬 "Has the transfer workflow completed yet?"
102 | 
103 | > 💬 "Run a transfer scenario with recoverable failures to test error handling"
104 | 
105 | Behind the scenes, Temporal MCP translates these natural language requests into properly formatted workflow executions—no more complex API calls or parameter formatting!
106 | 
107 | ## Core Values
108 | 
109 | 1. **Clarity First** — Use simple, direct language. Avoid jargon.
110 | 2. **Benefit-Driven** — Lead with "what's in it for me".
111 | 3. **Concise Power** — Less is more—keep sentences tight and memorable.
112 | 4. **Personality & Voice** — Bold statements, short lines, a dash of excitement.
113 | 
114 | ## Ready to Showcase
115 | 
116 | Lights, camera, action—capture your first AI-driven workflow in motion. Share that moment. Inspire others to see Temporal MCP in action.
117 | 
118 | ## Development
119 | 
120 | ### Project Structure
121 | 
122 | ```
123 | ./
124 | ├── cmd/            # Entry points for executables
125 | ├── internal/       # Internal package code
126 | │   ├── api/        # MCP API implementation
127 | │   ├── cache/      # Caching layer
128 | │   ├── config/     # Configuration management
129 | │   └── temporal/   # Temporal client integration
130 | ├── examples/       # Example configurations and scripts
131 | └── docs/           # Documentation
132 | ```
133 | 
134 | ### Common Commands
135 | 
136 | | Command | Description |
137 | |---------|-------------|
138 | | `make build` | Builds the binary in `./bin` |
139 | | `make test` | Runs all unit tests |
140 | | `make fmt` | Formats code according to Go standards |
141 | | `make run` | Builds and runs the server |
142 | | `make clean` | Removes build artifacts |
143 | 
144 | ## 🔍 Troubleshooting
145 | 
146 | ### Common Issues
147 | 
148 | **Connection Refused**
149 | - ✓ Check if Temporal server is running
150 | - ✓ Verify hostPort is correct in config.yml
151 | 
152 | **Workflow Not Found**
153 | - ✓ Ensure workflow is registered in Temporal
154 | - ✓ Check namespace settings in config.yml
155 | 
156 | **Claude Can't See Workflows**
157 | - ✓ Verify claude_config.json is in the correct location
158 | - ✓ Restart Claude after configuration changes
159 | 
160 | ## ⚙️ Configuration
161 | 
162 | The heart of Temporal MCP is its configuration file, which connects your AI assistants to your workflow engine:
163 | 
164 | ### Configuration Architecture
165 | 
166 | Your `config.yml` consists of three key sections:
167 | 
168 | 1. **🔌 Temporal Connection** — How to connect to your Temporal server
169 | 2. **💾 Cache Settings** — Performance optimization for workflow results
170 | 3. **🔧 Workflow Definitions** — The workflows your AI can discover and use
171 | 
172 | ### Example Configuration
173 | 
174 | The sample configuration is designed to work with the Temporal Money Transfer Demo:
175 | 
176 | ```yaml
177 | # Temporal server connection details
178 | temporal:
179 |   hostPort: "localhost:7233"       # Your Temporal server address
180 |   namespace: "default"             # Temporal namespace
181 |   environment: "local"             # "local" or "remote"
182 |   defaultTaskQueue: "account-transfer-queue"  # Default task queue for workflows
183 | 
184 |   # Fine-tune connection behavior
185 |   timeout: "5s"                    # Connection timeout
186 |   retryOptions:                     # Robust retry settings
187 |     initialInterval: "100ms"       # Start with quick retries
188 |     maximumInterval: "10s"         # Max wait between retries
189 |     maximumAttempts: 5              # Don't try forever
190 |     backoffCoefficient: 2.0         # Exponential backoff
191 | 
192 | # Define AI-discoverable workflows
193 | workflows:
194 |   AccountTransferWorkflow:
195 |     purpose: "Transfers money between accounts with validation and notification. Handles the happy path scenario where everything works as expected."
196 |     workflowIDRecipe: "transfer_{{.from_account}}_{{.to_account}}_{{.amount}}"
197 |     input:
198 |       type: "TransferInput"
199 |       fields:
200 |         - from_account: "Source account ID"
201 |         - to_account: "Destination account ID"
202 |         - amount: "Amount to transfer"
203 |     output:
204 |       type: "TransferOutput"
205 |       description: "Transfer confirmation with charge ID"
206 |     taskQueue: "account-transfer-queue"
207 |     activities:
208 |       - name: "validate"
209 |         timeout: "5s"
210 |       - name: "withdraw"
211 |         timeout: "5s"
212 |       - name: "deposit"
213 |         timeout: "5s"
214 |       - name: "sendNotification"
215 |         timeout: "5s"
216 |       - name: "undoWithdraw"
217 |         timeout: "5s"
218 | ```
219 | 
220 | > 💡 **Pro Tip:** The sample configuration is pre-configured to work with the [Temporal Money Transfer Demo](https://github.com/temporal-sa/money-transfer-demo/tree/main). Use it as a starting point for your own workflows.
221 | 
222 | ## 💎 Best Practices
223 | 
224 | ### Crafting Perfect Purpose Fields
225 | 
226 | The `purpose` field is your AI assistant's window into understanding what each workflow does. Make it count!
227 | 
228 | #### ✅ Do This
229 | - Write clear, detailed descriptions of functionality
230 | - Mention key parameters and how they customize behavior
231 | - Describe expected outputs and their formats
232 | - Note any limitations or constraints
233 | 
234 | #### ❌ Avoid This
235 | - Vague descriptions ("Processes data")
236 | - Technical jargon without explanation
237 | - Missing important parameters
238 | - Ignoring error cases or limitations
239 | 
240 | #### Before & After
241 | 
242 | **Before:** "Gets information about a file."
243 | 
244 | **After:** "Retrieves detailed metadata about a file or directory including size, creation time, last modified time, permissions, and type. Performs access validation to ensure the requested file is within allowed directories. Returns formatted JSON with all attributes or an appropriate error message."
245 | 
246 | ### Naming Conventions
247 | 
248 | | Item | Convention | Example |
249 | |------|------------|----------|
250 | | Workflow IDs | PascalCase | `AccountTransferWorkflow` |
251 | | Parameter names | snake_case | `from_account`, `to_account` |
252 | | Parameters with units | Include unit | `timeout_seconds`, `amount` |
253 | 
254 | ### Security Guidelines
255 | 
256 | ⚠️ **Important Security Notes:**
257 | 
258 | - Keep credentials out of your configuration files
259 | - Use environment variables for sensitive values
260 | - Consider access controls for workflows with sensitive data
261 | - Validate and sanitize all workflow inputs
262 | 
263 | > 💡 **Tip:** Create different configurations for development and production environments
264 | 
265 | ### Why Good Purpose Fields Matter
266 | 
267 | 1. **Enhanced AI Understanding** - Claude and other AI tools can provide much more accurate and helpful responses when they fully understand the capabilities and limitations of each component
268 | 2. **Fewer Errors** - Detailed descriptions reduce the chances of AI systems using components incorrectly
269 | 3. **Improved Debugging** - Clear descriptions help identify issues when workflows don't behave as expected
270 | 4. **Better Developer Experience** - New team members can understand your system more quickly
271 | 5. **Documentation As Code** - Purpose fields serve as living documentation that stays in sync with the codebase
272 | 
273 | ## Contribute & Collaborate
274 | 
275 | We're building this together.
276 | - Share your own workflow configs
277 | - Improve descriptions
278 | - Share your demos (video or GIF) in issues
279 | 
280 | Let's unleash the power of AI and Temporal together!
281 | 
282 | ## 📜 License
283 | 
284 | This project is licensed under the MIT License - see the LICENSE file for details.
285 | Contributions welcome!
286 | 
```
--------------------------------------------------------------------------------
/internal/tool/definition.go:
--------------------------------------------------------------------------------
```go
 1 | package tool
 2 | 
 3 | // Definition represents an MCP tool definition
 4 | type Definition struct {
 5 | 	Name        string `json:"name"`
 6 | 	Description string `json:"description"`
 7 | 	Parameters  Schema `json:"parameters"`
 8 | 	Internal    bool   `json:"-"` // Flag for internal tools like ClearCache
 9 | }
10 | 
11 | // Schema represents a JSON Schema for tool parameters
12 | type Schema struct {
13 | 	Type       string                    `json:"type"`
14 | 	Properties map[string]SchemaProperty `json:"properties"`
15 | 	Required   []string                  `json:"required"`
16 | }
17 | 
18 | // SchemaProperty represents a property in a JSON Schema
19 | type SchemaProperty struct {
20 | 	Type        string `json:"type"`
21 | 	Description string `json:"description"`
22 | }
23 | 
```
--------------------------------------------------------------------------------
/cmd/main.go:
--------------------------------------------------------------------------------
```go
 1 | package main
 2 | 
 3 | import (
 4 | 	"log"
 5 | 	"os"
 6 | 	"os/signal"
 7 | 	"syscall"
 8 | )
 9 | 
10 | func main() {
11 | 	// Configure logger to write to stderr
12 | 	log.SetOutput(os.Stderr)
13 | 	log.Println("Starting Temporal MCP...")
14 | 
15 | 	// Setup signal handling for graceful shutdown
16 | 	sigCh := make(chan os.Signal, 1)
17 | 	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
18 | 
19 | 	// TODO: Initialize configuration
20 | 	// TODO: Setup Temporal client
21 | 	// TODO: Initialize services
22 | 	// TODO: Start API server
23 | 
24 | 	log.Println("Temporal MCP is running. Press Ctrl+C to stop.")
25 | 
26 | 	// Wait for termination signal
27 | 	sig := <-sigCh
28 | 	log.Printf("Received signal %v, shutting down...", sig)
29 | 
30 | 	// TODO: Perform cleanup and graceful shutdown
31 | 
32 | 	log.Println("Temporal MCP has been stopped.")
33 | }
34 | 
```
--------------------------------------------------------------------------------
/internal/temporal/logger.go:
--------------------------------------------------------------------------------
```go
 1 | package temporal
 2 | 
 3 | import (
 4 | 	"log"
 5 | )
 6 | 
 7 | // StderrLogger implements the Temporal logger interface
 8 | // ensuring all Temporal logs go to stderr instead of stdout
 9 | type StderrLogger struct {
10 | 	logger *log.Logger
11 | }
12 | 
13 | // Debug logs a debug message
14 | func (l *StderrLogger) Debug(msg string, keyvals ...interface{}) {
15 | 	l.logger.Printf("[DEBUG] %s", msg)
16 | }
17 | 
18 | // Info logs an info message
19 | func (l *StderrLogger) Info(msg string, keyvals ...interface{}) {
20 | 	l.logger.Printf("[INFO] %s", msg)
21 | }
22 | 
23 | // Warn logs a warning message
24 | func (l *StderrLogger) Warn(msg string, keyvals ...interface{}) {
25 | 	l.logger.Printf("[WARN] %s", msg)
26 | }
27 | 
28 | // Error logs an error message
29 | func (l *StderrLogger) Error(msg string, keyvals ...interface{}) {
30 | 	l.logger.Printf("[ERROR] %s", msg)
31 | }
32 | 
```
--------------------------------------------------------------------------------
/.github/workflows/go-test.yml:
--------------------------------------------------------------------------------
```yaml
 1 | name: Go Tests
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [ main ]
 6 |   pull_request:
 7 |     branches: [ main ]
 8 | 
 9 | jobs:
10 |   test:
11 |     name: Run Tests
12 |     runs-on: ubuntu-latest
13 |     steps:
14 |     - name: Check out code
15 |       uses: actions/checkout@v3
16 | 
17 |     - name: Set up Go
18 |       uses: actions/setup-go@v4
19 |       with:
20 |         go-version: '1.21'
21 |         cache: true
22 | 
23 |     - name: Install dependencies
24 |       run: go mod download
25 | 
26 |     - name: Verify dependencies
27 |       run: go mod verify
28 | 
29 |     - name: Run tests
30 |       run: go test -v ./...
31 | 
32 |     - name: Run formatting check
33 |       run: |
34 |         if [ -n "$(gofmt -l .)" ]; then
35 |           echo "The following files need to be formatted:"
36 |           gofmt -l .
37 |           exit 1
38 |         fi
39 | 
40 |     - name: Run vet
41 |       run: go vet ./...
42 | 
```
--------------------------------------------------------------------------------
/cmd/temporal-mcp/hash_args.go:
--------------------------------------------------------------------------------
```go
 1 | package main
 2 | 
 3 | import (
 4 | 	"encoding/json"
 5 | 	"fmt"
 6 | 	"hash/fnv"
 7 | 	"log"
 8 | )
 9 | 
10 | // hashWorkflowArgs produces a short (suitable for inclusion in workflow id) hash of the given arguments. Args must be
11 | // json.Marshal-able.
12 | func hashWorkflowArgs(allParams map[string]string, paramsToHash ...any) (string, error) {
13 | 	if len(paramsToHash) == 0 {
14 | 		log.Printf("Warning: No hash arguments provided - will hash all arguments. Please replace {{ hash }} with {{ hash . }} in the workflowIDRecipe")
15 | 		paramsToHash = []any{allParams}
16 | 	}
17 | 
18 | 	hasher := fnv.New32()
19 | 	for _, arg := range paramsToHash {
20 | 		// important: json.Marshal sorts map keys
21 | 		bytes, err := json.Marshal(arg)
22 | 		if err != nil {
23 | 			return "", err
24 | 		}
25 | 		_, _ = hasher.Write(bytes)
26 | 	}
27 | 	return fmt.Sprintf("%d", hasher.Sum32()), nil
28 | }
29 | 
```
--------------------------------------------------------------------------------
/internal/tool/registry.go:
--------------------------------------------------------------------------------
```go
 1 | // Package tool provides utilities for working with Temporal workflows as MCP tools
 2 | package tool
 3 | 
 4 | import (
 5 | 	"github.com/mocksi/temporal-mcp/internal/config"
 6 | 	"go.temporal.io/sdk/client"
 7 | )
 8 | 
 9 | // Registry manages workflow tools metadata and dependencies
10 | type Registry struct {
11 | 	config     *config.Config
12 | 	tempClient client.Client
13 | }
14 | 
15 | // NewRegistry creates a new tool registry with required dependencies
16 | func NewRegistry(cfg *config.Config, tempClient client.Client) *Registry {
17 | 	return &Registry{
18 | 		config:     cfg,
19 | 		tempClient: tempClient,
20 | 	}
21 | }
22 | 
23 | // GetConfig returns the configuration used by this registry
24 | func (r *Registry) GetConfig() *config.Config {
25 | 	return r.config
26 | }
27 | 
28 | // GetTemporalClient returns the Temporal client instance
29 | func (r *Registry) GetTemporalClient() client.Client {
30 | 	return r.tempClient
31 | }
32 | 
```
--------------------------------------------------------------------------------
/internal/tool/registry_test.go:
--------------------------------------------------------------------------------
```go
 1 | package tool
 2 | 
 3 | import (
 4 | 	"testing"
 5 | 
 6 | 	"github.com/mocksi/temporal-mcp/internal/config"
 7 | )
 8 | 
 9 | // TestRegistryGetters tests the Registry getters
10 | func TestRegistryGetters(t *testing.T) {
11 | 	// Create test objects
12 | 	cfg := &config.Config{}
13 | 
14 | 	// Create registry directly without using interfaces to avoid lint errors
15 | 	registry := &Registry{
16 | 		config: cfg,
17 | 	}
18 | 
19 | 	// Test GetConfig
20 | 	if registry.GetConfig() != cfg {
21 | 		t.Error("GetConfig did not return the expected config")
22 | 	}
23 | 
24 | 	// For GetTemporalClient, we can only check it's not nil
25 | 	// since we can't directly compare interface values
26 | 	// Skip testing GetTemporalClient to avoid interface implementation issues
27 | }
28 | 
29 | // TestNewRegistry tests the NewRegistry constructor
30 | func TestNewRegistry(t *testing.T) {
31 | 	// Create test objects
32 | 	cfg := &config.Config{}
33 | 
34 | 	// Since we can't easily mock the client.Client interface in tests,
35 | 	// we'll create the registry directly instead of using NewRegistry
36 | 
37 | 	// Create a registry directly
38 | 	registry := &Registry{
39 | 		config: cfg,
40 | 	}
41 | 
42 | 	// Test just the config and cacheClient properties
43 | 	if registry.config != cfg {
44 | 		t.Error("Registry not initialized with the correct config")
45 | 	}
46 | }
47 | 
```
--------------------------------------------------------------------------------
/internal/config/config.go:
--------------------------------------------------------------------------------
```go
 1 | package config
 2 | 
 3 | import (
 4 | 	"gopkg.in/yaml.v3"
 5 | 	"os"
 6 | )
 7 | 
 8 | // Config holds the top-level configuration
 9 | type Config struct {
10 | 	Temporal  TemporalConfig         `yaml:"temporal"`
11 | 	Workflows map[string]WorkflowDef `yaml:"workflows"`
12 | }
13 | 
14 | // TemporalConfig defines connection settings for Temporal service
15 | type TemporalConfig struct {
16 | 	HostPort         string `yaml:"hostPort"`
17 | 	Namespace        string `yaml:"namespace"`
18 | 	Environment      string `yaml:"environment"`
19 | 	Timeout          string `yaml:"timeout,omitempty"`
20 | 	DefaultTaskQueue string `yaml:"defaultTaskQueue,omitempty"`
21 | }
22 | 
23 | // WorkflowDef describes a Temporal workflow exposed as a tool
24 | type WorkflowDef struct {
25 | 	Purpose          string       `yaml:"purpose"`
26 | 	Input            ParameterDef `yaml:"input"`
27 | 	Output           ParameterDef `yaml:"output"`
28 | 	TaskQueue        string       `yaml:"taskQueue"`
29 | 	WorkflowIDRecipe string       `yaml:"workflowIDRecipe"`
30 | }
31 | 
32 | // ParameterDef defines input/output schema for a workflow
33 | type ParameterDef struct {
34 | 	Type        string              `yaml:"type"`
35 | 	Fields      []map[string]string `yaml:"fields"`
36 | 	Description string              `yaml:"description,omitempty"`
37 | }
38 | 
39 | // LoadConfig reads and parses YAML config from file
40 | func LoadConfig(path string) (*Config, error) {
41 | 	data, err := os.ReadFile(path)
42 | 	if err != nil {
43 | 		return nil, err
44 | 	}
45 | 	var cfg Config
46 | 	if err := yaml.Unmarshal(data, &cfg); err != nil {
47 | 		return nil, err
48 | 	}
49 | 	return &cfg, nil
50 | }
51 | 
```
--------------------------------------------------------------------------------
/internal/temporal/client.go:
--------------------------------------------------------------------------------
```go
 1 | package temporal
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"log"
 6 | 	"os"
 7 | 	"time"
 8 | 
 9 | 	"github.com/mocksi/temporal-mcp/internal/config"
10 | 	"go.temporal.io/sdk/client"
11 | )
12 | 
13 | // NewTemporalClient creates a Temporal client based on the provided configuration
14 | func NewTemporalClient(cfg config.TemporalConfig) (client.Client, error) {
15 | 	// Validate timeout format if specified
16 | 	if cfg.Timeout != "" {
17 | 		_, err := time.ParseDuration(cfg.Timeout)
18 | 		if err != nil {
19 | 			return nil, fmt.Errorf("invalid timeout format: %w", err)
20 | 		}
21 | 		// Note: We're only validating the format, actual timeout handling would be implemented here
22 | 	}
23 | 
24 | 	// Configure a logger that uses stderr
25 | 	tempLogger := log.New(os.Stderr, "[temporal] ", log.LstdFlags)
26 | 
27 | 	// Create Temporal logger adapter that ensures all logs go to stderr
28 | 	temporalLogger := &StderrLogger{logger: tempLogger}
29 | 
30 | 	// Set client options
31 | 	options := client.Options{
32 | 		HostPort:  cfg.HostPort,
33 | 		Namespace: cfg.Namespace,
34 | 		Logger:    temporalLogger,
35 | 	}
36 | 
37 | 	// Handle environment-specific configuration
38 | 	switch cfg.Environment {
39 | 	case "local":
40 | 		// Local Temporal server (default settings)
41 | 	case "remote":
42 | 		// To be implemented for remote/cloud Temporal connections
43 | 		// This would include TLS and authentication setup
44 | 		return nil, fmt.Errorf("remote environment configuration not implemented yet")
45 | 	default:
46 | 		return nil, fmt.Errorf("unsupported environment type: %s", cfg.Environment)
47 | 	}
48 | 
49 | 	// Create the client
50 | 	temporalClient, err := client.Dial(options)
51 | 	if err != nil {
52 | 		return nil, fmt.Errorf("failed to create Temporal client: %w", err)
53 | 	}
54 | 
55 | 	return temporalClient, nil
56 | }
57 | 
```
--------------------------------------------------------------------------------
/examples/generate_claude_config.sh:
--------------------------------------------------------------------------------
```bash
 1 | #!/bin/bash
 2 | 
 3 | # Script to generate a claude_config.json file with correct paths
 4 | # This should be run from the examples directory
 5 | 
 6 | set -e  # Exit on error
 7 | 
 8 | # Get the parent directory of the examples folder
 9 | PARENT_DIR="$(cd .. && pwd)"
10 | 
11 | # Define the output file
12 | CONFIG_FILE="claude_config.json"
13 | 
14 | # Check if we're in the examples directory
15 | if [[ "$(basename $(pwd))" != "examples" ]]; then
16 |   echo "Error: This script must be run from the examples directory"
17 |   exit 1
18 | fi
19 | 
20 | # Check if binary exists
21 | if [[ ! -f "$PARENT_DIR/bin/temporal-mcp" ]]; then
22 |   echo "Warning: temporal-mcp binary not found. Make sure to build it first with 'make build'"
23 | fi
24 | 
25 | # Generate the JSON configuration file
26 | cat > "$CONFIG_FILE" << EOF
27 | {
28 |   "mcpServers": {
29 |     "temporal-mcp": {
30 |       "command": "$PARENT_DIR/bin/temporal-mcp",
31 |       "args": ["--config", "$PARENT_DIR/config.yml"],
32 |       "env": {}
33 |     }
34 |   }
35 | }
36 | EOF
37 | 
38 | echo "Generated $CONFIG_FILE with correct paths"
39 | 
40 | # Add file to .gitignore if it's not already there
41 | GITIGNORE_FILE="$PARENT_DIR/.gitignore"
42 | 
43 | if [[ -f "$GITIGNORE_FILE" ]]; then
44 |   if ! grep -q "examples/$CONFIG_FILE" "$GITIGNORE_FILE"; then
45 |     echo "Adding $CONFIG_FILE to .gitignore"
46 |     echo "examples/$CONFIG_FILE" >> "$GITIGNORE_FILE"
47 |   else
48 |     echo "$CONFIG_FILE is already in .gitignore"
49 |   fi
50 | else
51 |   echo "Creating .gitignore and adding $CONFIG_FILE"
52 |   echo "examples/$CONFIG_FILE" > "$GITIGNORE_FILE"
53 | fi
54 | 
55 | # Instructions for the user
56 | echo ""
57 | echo "To use this configuration with Claude:"
58 | echo "1. Copy this file to Claude's configuration directory:"
59 | echo "   cp $CONFIG_FILE ~/Library/Application\\ Support/Claude/claude_desktop_config.json"
60 | echo "2. Restart Claude if it's already running"
61 | echo ""
62 | echo "Alternatively, you can reference this file in your Claude installation settings."
63 | echo "See the README.md for more information."
64 | 
65 | # Make the script executable
66 | chmod +x "$0"
67 | 
```
--------------------------------------------------------------------------------
/internal/sanitize_history_event/sanitize_history_event.go:
--------------------------------------------------------------------------------
```go
 1 | package sanitize_history_event
 2 | 
 3 | import (
 4 | 	"go.temporal.io/api/history/v1"
 5 | 	"google.golang.org/protobuf/reflect/protoreflect"
 6 | 	"strings"
 7 | )
 8 | 
 9 | // SanitizeHistoryEvent removes all Payloads from the given history event's attributes. This helps mitigate the impact of
10 | // large workflow histories (temporal permits up to 50mb) on small LLM context windows (~2mb). This is just best
11 | // effort - it assumes that largeness is caused by the payloads.
12 | func SanitizeHistoryEvent(event *history.HistoryEvent) {
13 | 	sanitizeRecursively(event.ProtoReflect())
14 | }
15 | 
16 | // HistoryEvents are highly polymorphic (today: 54 different types), and Temporal could add new types at any time (most
17 | // recent time: launching Nexus). Let's sanitize via convention, rather than a hard-coded list of history event types
18 | // and their structure.
19 | func sanitizeRecursively(m protoreflect.Message) {
20 | 	m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
21 | 		switch {
22 | 		case fd.IsList():
23 | 			// Avoid lists of non-messages
24 | 			if fd.Kind() != protoreflect.MessageKind {
25 | 				return true
26 | 			}
27 | 
28 | 			list := v.List()
29 | 			for i := 0; i < list.Len(); i++ {
30 | 				item := list.Get(i).Message()
31 | 				if isPayload(item) {
32 | 					// Proto lists are homogeneous - if any items are payloads, all items are payloads
33 | 					list.Truncate(0)
34 | 				} else {
35 | 					sanitizeRecursively(item)
36 | 				}
37 | 			}
38 | 		case fd.IsMap():
39 | 			// Avoid maps of non-messages
40 | 			if fd.MapValue().Kind() != protoreflect.MessageKind {
41 | 				return true
42 | 			}
43 | 
44 | 			mapp := v.Map()
45 | 			mapp.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool {
46 | 				val := v.Message()
47 | 				if isPayload(val) {
48 | 					mapp.Clear(k)
49 | 				} else {
50 | 					sanitizeRecursively(val)
51 | 				}
52 | 
53 | 				return true
54 | 			})
55 | 		default:
56 | 			if fd.Kind() == protoreflect.MessageKind {
57 | 				msg := v.Message()
58 | 				if isPayload(msg) {
59 | 					m.Clear(fd)
60 | 				} else {
61 | 					sanitizeRecursively(msg)
62 | 				}
63 | 			}
64 | 		}
65 | 
66 | 		return true
67 | 	})
68 | }
69 | 
70 | func isPayload(m protoreflect.Message) bool {
71 | 	fullType := string(m.Descriptor().FullName())
72 | 	return strings.HasSuffix(fullType, ".Payload") || strings.HasSuffix(fullType, ".Payloads")
73 | }
74 | 
```
--------------------------------------------------------------------------------
/config.sample.yml:
--------------------------------------------------------------------------------
```yaml
 1 | temporal:
 2 |   # Connection configuration
 3 |   hostPort: "localhost:7233"  # Local Temporal server
 4 |   namespace: "default"
 5 |   environment: "local"        # "local" or "remote"
 6 |   defaultTaskQueue: "account-transfer-queue"  # Default task queue for workflows
 7 | 
 8 |   # Connection options
 9 |   timeout: "5s"
10 |   retryOptions:
11 |     initialInterval: "100ms"
12 |     maximumInterval: "10s"
13 |     maximumAttempts: 5
14 |     backoffCoefficient: 2.0
15 | 
16 | workflows:
17 |   AccountTransferWorkflow:
18 |     purpose: "Transfers money between accounts with validation and notification."
19 |     workflowIDRecipe: "transfer_{{.from_account}}_{{.to_account}}_{{.amount}}"
20 |     input:
21 |       type: "TransferInput"
22 |       fields:
23 |         - from_account: "Source account ID"
24 |         - to_account: "Destination account ID"
25 |         - amount: "Amount to transfer"
26 |     output:
27 |       type: "TransferOutput"
28 |       description: "Transfer confirmation with charge ID"
29 |     taskQueue: "account-transfer-queue"
30 |     activities:
31 |       - name: "validate"
32 |         timeout: "5s"
33 |       - name: "withdraw"
34 |         timeout: "5s"
35 |       - name: "deposit"
36 |         timeout: "5s"
37 |       - name: "sendNotification"
38 |         timeout: "5s"
39 |       - name: "undoWithdraw"
40 |         timeout: "5s"
41 | 
42 |   AccountTransferWorkflowScenarios:
43 |     purpose: "Extended account transfer workflow with approval and error handling scenarios."
44 |     workflowIDRecipe: "transfer_scenario_{{.scenario_type}}_{{.from_account}}_{{.to_account}}"
45 |     input:
46 |       type: "TransferInput"
47 |       fields:
48 |         - from_account: "Source account ID"
49 |         - to_account: "Destination account ID"
50 |         - amount: "Amount to transfer"
51 |     output:
52 |       type: "TransferOutput"
53 |       description: "Transfer confirmation with charge ID"
54 |     taskQueue: "account-transfer-queue"
55 |     scenarios:
56 |       - name: "AccountTransferWorkflowRecoverableFailure"
57 |         description: "Simulates a recoverable failure scenario"
58 |       - name: "AccountTransferWorkflowHumanInLoop"
59 |         description: "Requires human approval before proceeding"
60 |         approvalTimeout: "30s"
61 |       - name: "AccountTransferWorkflowAdvancedVisibility"
62 |         description: "Includes advanced visibility features"
63 |     activities:
64 |       - name: "validate"
65 |         timeout: "5s"
66 |       - name: "withdraw"
67 |         timeout: "5s"
68 |       - name: "deposit"
69 |         timeout: "5s"
70 |       - name: "sendNotification"
71 |         timeout: "5s"
72 |       - name: "undoWithdraw"
73 |         timeout: "5s"
74 | 
```
--------------------------------------------------------------------------------
/internal/config/config_test.go:
--------------------------------------------------------------------------------
```go
  1 | package config
  2 | 
  3 | import (
  4 | 	"os"
  5 | 	"path/filepath"
  6 | 	"testing"
  7 | )
  8 | 
  9 | func TestLoadConfig(t *testing.T) {
 10 | 	// Create a temporary config file
 11 | 	configPath := filepath.Join(t.TempDir(), "test_config.yml")
 12 | 
 13 | 	// Sample YAML content matching our struct definitions
 14 | 	configContent := `
 15 | temporal:
 16 |   hostPort: "localhost:7233"
 17 |   namespace: "default"
 18 |   environment: "local"
 19 | 
 20 | workflows:
 21 |   TestWorkflow:
 22 |     purpose: "Test workflow"
 23 |     input:
 24 |       type: "TestRequest"
 25 |       fields:
 26 |         - id: "The test ID"
 27 |         - name: "The test name"
 28 |         - data: "Test data payload"
 29 |     output:
 30 |       type: "string"
 31 |       description: "Test result"
 32 |     taskQueue: "test-queue"
 33 | `
 34 | 	// Write the test config
 35 | 	err := os.WriteFile(configPath, []byte(configContent), 0644)
 36 | 	if err != nil {
 37 | 		t.Fatalf("Failed to write test config: %v", err)
 38 | 	}
 39 | 
 40 | 	// Load the config
 41 | 	cfg, err := LoadConfig(configPath)
 42 | 	if err != nil {
 43 | 		t.Fatalf("Failed to load config: %v", err)
 44 | 	}
 45 | 
 46 | 	// Validate the loaded config
 47 | 	if cfg.Temporal.HostPort != "localhost:7233" {
 48 | 		t.Errorf("Expected HostPort to be localhost:7233, got %s", cfg.Temporal.HostPort)
 49 | 	}
 50 | 
 51 | 	if cfg.Temporal.Namespace != "default" {
 52 | 		t.Errorf("Expected Namespace to be default, got %s", cfg.Temporal.Namespace)
 53 | 	}
 54 | 
 55 | 	workflow, exists := cfg.Workflows["TestWorkflow"]
 56 | 	if !exists {
 57 | 		t.Fatal("TestWorkflow not found in config")
 58 | 	}
 59 | 
 60 | 	if workflow.Purpose != "Test workflow" {
 61 | 		t.Errorf("Expected workflow purpose to be 'Test workflow', got '%s'", workflow.Purpose)
 62 | 	}
 63 | 
 64 | 	if workflow.TaskQueue != "test-queue" {
 65 | 		t.Errorf("Expected task queue to be 'test-queue', got '%s'", workflow.TaskQueue)
 66 | 	}
 67 | 
 68 | 	if len(workflow.Input.Fields) != 3 {
 69 | 		t.Fatalf("Expected 3 input fields, got %d", len(workflow.Input.Fields))
 70 | 	}
 71 | 
 72 | 	if _, ok := workflow.Input.Fields[0]["id"]; !ok {
 73 | 		t.Error("Expected input field 'id' not found")
 74 | 	}
 75 | 
 76 | 	if _, ok := workflow.Input.Fields[1]["name"]; !ok {
 77 | 		t.Error("Expected input field 'name' not found")
 78 | 	}
 79 | 
 80 | 	if _, ok := workflow.Input.Fields[2]["data"]; !ok {
 81 | 		t.Error("Expected input field 'data' not found")
 82 | 	}
 83 | }
 84 | 
 85 | // TestWorkflowInputStructs verifies that workflow input configuration correctly maps to expected struct fields
 86 | func TestWorkflowInputStructs(t *testing.T) {
 87 | 	// Create a test workflow definition with specific input fields
 88 | 	wf := WorkflowDef{
 89 | 		Purpose: "Test input fields",
 90 | 		Input: ParameterDef{
 91 | 			Type: "TestRequest",
 92 | 			Fields: []map[string]string{
 93 | 				{"id": "The unique identifier"},
 94 | 				{"name": "The name field"},
 95 | 				{"data": "JSON payload data"},
 96 | 			},
 97 | 		},
 98 | 	}
 99 | 
100 | 	// Verify input field structure
101 | 	if len(wf.Input.Fields) != 3 {
102 | 		t.Fatalf("Expected 3 input fields, got %d", len(wf.Input.Fields))
103 | 	}
104 | 
105 | 	// Verify fields match expected keys
106 | 	expectedFields := []string{"id", "name", "data"}
107 | 	for i, expectedField := range expectedFields {
108 | 		field := wf.Input.Fields[i]
109 | 		found := false
110 | 		for key := range field {
111 | 			if key == expectedField {
112 | 				found = true
113 | 				break
114 | 			}
115 | 		}
116 | 		if !found {
117 | 			t.Errorf("Expected field '%s' not found at position %d", expectedField, i)
118 | 		}
119 | 	}
120 | }
121 | 
```
--------------------------------------------------------------------------------
/docs/VERSION_0-project.md:
--------------------------------------------------------------------------------
```markdown
 1 | # VERSION_0 Project Tasks
 2 | 
 3 | This document breaks down the VERSION_0 specification into small, measurable, actionable tasks.
 4 | 
 5 | ## 1. Project Setup
 6 | - [x] Add dependencies in `go.mod`:
 7 |   - `go.temporal.io/sdk`
 8 |   - `gopkg.in/yaml.v3`
 9 |   - `github.com/mattn/go-sqlite3`
10 | - [x] Install dependencies via Make: `make install`
11 | - [x] Build the application: `make build`
12 | 
13 | ## 2. Config Parser
14 | - [x] Define Go struct types in `config.go`:
15 |   - `Config`, `TemporalConfig`, `CacheConfig`, `WorkflowDef`
16 | - [x] Implement `func LoadConfig(path string) (*Config, error)` in `config.go`
17 | - [x] Write unit test `TestLoadConfig` in `config_test.go` using a sample YAML file
18 | 
19 | ## 3. Temporal Client
20 | - [x] Implement `func NewTemporalClient(cfg TemporalConfig) (client.Client, error)` in `temporal.go`
21 | - [x] Write unit test `TestNewTemporalClient` with a stubbed `client.Dial`
22 | 
23 | ## 4. Tool Registry
24 | - [x] Implement workflow tool registration
25 | - [x] Support dynamic tool definitions based on config
26 | - [x] Add default task queue support
27 | - [x] Write unit tests for task queue selection
28 | 
29 | ## 5. MCP Protocol Handler
30 | - [x] Implement MCP server using `mcp-golang` library
31 | - [x] Add workflow tool registration and execution
32 | - [x] Add system prompt registration
33 | - [x] Implement graceful error handling for Temporal connection failures
34 | 
35 | ## 6. Cache Manager
36 | - [x] Implement `CacheClient` with methods `Get`, `Set`, and `Clear`
37 | - [x] Initialize SQLite database with TTL and max size parameters
38 | - [x] Write unit tests for cache functionality
39 | 
40 | ## 7. ClearCache Tool
41 | - [x] Add `ClearCache` tool definition
42 | - [x] Implement handler for ClearCache calling `CacheClient.Clear`
43 | - [x] Write tests for ClearCache functionality
44 | 
45 | ## 8. Example Configuration
46 | - [x] Create configuration examples (`config.yml` and `config.sample.yml`)
47 | - [x] Add MCP configuration examples in `/examples` directory
48 | - [x] Validate `LoadConfig` parses configuration correctly
49 | 
50 | ## 9. Security & Validation
51 | - [x] Add parameter validation for workflow tools
52 | - [x] Implement safe error handling for failed workflows
53 | - [x] Ensure all logging goes to stderr to avoid corrupting the JSON-RPC protocol
54 | 
55 | ## 10. Performance Benchmarking
56 | - [ ] Add benchmark `BenchmarkToolDiscovery` in `benchmarks/tool_discovery_test.go` to verify <100ms discovery
57 | - [ ] Add benchmark `BenchmarkToolInvocation` in `benchmarks/tool_invocation_test.go` to verify <200ms invocation
58 | 
59 | ## 11. Testing & CI
60 | - [x] Add `make test` target to run all unit tests
61 | - [x] Configure a CI workflow (e.g., GitHub Actions) to run tests on push and PR events
62 | 
63 | ## 12. Documentation
64 | - [x] Update `README.md` with project overview
65 | - [x] Create documentation for setup and configuration instructions
66 | - [x] Document how to run the MCP server
67 | - [x] Document example usage with Claude in `examples/README.md`
68 | 
69 | ## 13. Integration with Claude
70 | - [x] Add example configuration for Claude Desktop in `examples/claude_config.json`
71 | - [x] Provide comprehensive examples for temporal-mcp usage
72 | - [x] Fix logging to ensure proper JSON-RPC communication
73 | - [x] Update build system to use `./bin` directory for binaries
```
--------------------------------------------------------------------------------
/internal/sanitize_history_event/sanitize_history_event_test.go:
--------------------------------------------------------------------------------
```go
  1 | package sanitize_history_event
  2 | 
  3 | import (
  4 | 	"bufio"
  5 | 	"context"
  6 | 	"fmt"
  7 | 	"github.com/mocksi/temporal-mcp/internal/config"
  8 | 	"github.com/mocksi/temporal-mcp/internal/temporal"
  9 | 	"github.com/stretchr/testify/require"
 10 | 	temporal_enums "go.temporal.io/api/enums/v1"
 11 | 	"go.temporal.io/api/history/v1"
 12 | 	"google.golang.org/protobuf/encoding/protojson"
 13 | 	"os"
 14 | 	"strings"
 15 | 	"testing"
 16 | )
 17 | 
 18 | const TEST_DIR = "test_data"
 19 | const ORIGINAL_SUFFIX = "_original.jsonl"
 20 | 
 21 | func TestSanitizeHistoryEvent(t *testing.T) {
 22 | 	// To generate new test files from a real workflow history, uncomment the following line
 23 | 	// generateTestJson(t, "localhost:7233", "default", "someWorkflowID")
 24 | 
 25 | 	workflowIDs := make([]string, 0)
 26 | 	entries, err := os.ReadDir(TEST_DIR)
 27 | 	require.NoError(t, err)
 28 | 	for _, entry := range entries {
 29 | 		if entry.IsDir() {
 30 | 			continue
 31 | 		}
 32 | 
 33 | 		if strings.HasSuffix(entry.Name(), ORIGINAL_SUFFIX) {
 34 | 			workflowIDs = append(workflowIDs, entry.Name()[0:len(entry.Name())-len(ORIGINAL_SUFFIX)])
 35 | 		}
 36 | 	}
 37 | 
 38 | 	for _, workflowID := range workflowIDs {
 39 | 		t.Run(fmt.Sprintf("history for %s", workflowID), func(t *testing.T) {
 40 | 			original, sanitized := getTestFilenames(workflowID)
 41 | 
 42 | 			originalEvents := readEvents(t, original)
 43 | 			sanitizedEvents := readEvents(t, sanitized)
 44 | 			require.Equal(t, len(originalEvents), len(sanitizedEvents))
 45 | 
 46 | 			for i, actualEvent := range originalEvents {
 47 | 				SanitizeHistoryEvent(actualEvent)
 48 | 				require.Equal(t, sanitizedEvents[i], actualEvent)
 49 | 			}
 50 | 		})
 51 | 	}
 52 | }
 53 | 
 54 | func generateTestJson(t *testing.T, hostport string, namespace string, workflowID string) {
 55 | 	tClient, err := temporal.NewTemporalClient(config.TemporalConfig{
 56 | 		HostPort:         hostport,
 57 | 		Namespace:        namespace,
 58 | 		Environment:      "local",
 59 | 		DefaultTaskQueue: "unused",
 60 | 	})
 61 | 	require.NoError(t, err)
 62 | 
 63 | 	iter := tClient.GetWorkflowHistory(context.Background(), workflowID, "", false, temporal_enums.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT)
 64 | 
 65 | 	original, sanitized := getTestFilenames(workflowID)
 66 | 
 67 | 	originalFile, err := os.Create(original)
 68 | 	require.NoError(t, err)
 69 | 	defer originalFile.Close()
 70 | 
 71 | 	sanitizedFile, err := os.Create(sanitized)
 72 | 	require.NoError(t, err)
 73 | 	defer sanitizedFile.Close()
 74 | 
 75 | 	for iter.HasNext() {
 76 | 		event, err := iter.Next()
 77 | 		require.NoError(t, err)
 78 | 
 79 | 		writeEvent(t, originalFile, event)
 80 | 		SanitizeHistoryEvent(event)
 81 | 		writeEvent(t, sanitizedFile, event)
 82 | 	}
 83 | }
 84 | 
 85 | func writeEvent(t *testing.T, file *os.File, event *history.HistoryEvent) {
 86 | 	bytes, err := protojson.Marshal(event)
 87 | 	require.NoError(t, err)
 88 | 
 89 | 	bytes = append(bytes, '\n')
 90 | 
 91 | 	n, err := file.Write(bytes)
 92 | 	require.NoError(t, err)
 93 | 	require.Equal(t, len(bytes), n)
 94 | }
 95 | 
 96 | func readEvents(t *testing.T, filename string) []*history.HistoryEvent {
 97 | 	f, err := os.Open(filename)
 98 | 	require.NoError(t, err)
 99 | 	defer f.Close()
100 | 
101 | 	var events []*history.HistoryEvent
102 | 	scanner := bufio.NewScanner(f)
103 | 	for scanner.Scan() {
104 | 		eventJson := scanner.Text()
105 | 		event := &history.HistoryEvent{}
106 | 		err := protojson.Unmarshal([]byte(eventJson), event)
107 | 		require.NoError(t, err)
108 | 		events = append(events, event)
109 | 	}
110 | 
111 | 	return events
112 | }
113 | 
114 | func getTestFilenames(workflowID string) (string, string) {
115 | 	original := fmt.Sprintf("%s/%s%s", TEST_DIR, workflowID, ORIGINAL_SUFFIX)
116 | 	sanitized := fmt.Sprintf("%s/%s_sanitized.jsonl", TEST_DIR, workflowID)
117 | 	return original, sanitized
118 | }
119 | 
```
--------------------------------------------------------------------------------
/cmd/temporal-mcp/main_test.go:
--------------------------------------------------------------------------------
```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"github.com/stretchr/testify/require"
  6 | 	"testing"
  7 | 
  8 | 	"github.com/mocksi/temporal-mcp/internal/config"
  9 | )
 10 | 
 11 | // TestGetTaskQueue tests the task queue selection logic
 12 | func TestGetTaskQueue(t *testing.T) {
 13 | 	// Test cases to check task queue selection
 14 | 	tests := []struct {
 15 | 		name              string
 16 | 		workflowQueue     string
 17 | 		defaultQueue      string
 18 | 		expectedQueue     string
 19 | 		expectedIsDefault bool
 20 | 	}{
 21 | 		{
 22 | 			name:              "Workflow with specific task queue",
 23 | 			workflowQueue:     "specific-queue",
 24 | 			defaultQueue:      "default-queue",
 25 | 			expectedQueue:     "specific-queue",
 26 | 			expectedIsDefault: false,
 27 | 		},
 28 | 		{
 29 | 			name:              "Workflow without task queue uses default",
 30 | 			workflowQueue:     "",
 31 | 			defaultQueue:      "default-queue",
 32 | 			expectedQueue:     "default-queue",
 33 | 			expectedIsDefault: true,
 34 | 		},
 35 | 		{
 36 | 			name:              "Empty default with empty workflow queue",
 37 | 			workflowQueue:     "",
 38 | 			defaultQueue:      "",
 39 | 			expectedQueue:     "", // Empty because both are empty
 40 | 			expectedIsDefault: true,
 41 | 		},
 42 | 	}
 43 | 
 44 | 	// Run test cases
 45 | 	for _, tc := range tests {
 46 | 		t.Run(tc.name, func(t *testing.T) {
 47 | 			// Setup workflow and config
 48 | 			workflow := config.WorkflowDef{
 49 | 				TaskQueue: tc.workflowQueue,
 50 | 			}
 51 | 			cfg := &config.Config{
 52 | 				Temporal: config.TemporalConfig{
 53 | 					DefaultTaskQueue: tc.defaultQueue,
 54 | 				},
 55 | 			}
 56 | 
 57 | 			// Test task queue selection
 58 | 			taskQueue := workflow.TaskQueue
 59 | 			isUsingDefault := false
 60 | 
 61 | 			if taskQueue == "" {
 62 | 				taskQueue = cfg.Temporal.DefaultTaskQueue
 63 | 				isUsingDefault = true
 64 | 			}
 65 | 
 66 | 			// Verify results
 67 | 			if taskQueue != tc.expectedQueue {
 68 | 				t.Errorf("Expected task queue '%s', got '%s'", tc.expectedQueue, taskQueue)
 69 | 			}
 70 | 
 71 | 			if isUsingDefault != tc.expectedIsDefault {
 72 | 				t.Errorf("Expected isUsingDefault to be %v, got %v", tc.expectedIsDefault, isUsingDefault)
 73 | 			}
 74 | 		})
 75 | 	}
 76 | }
 77 | 
 78 | // TestTaskQueueOverride ensures workflow-specific task queue overrides default
 79 | func TestTaskQueueOverride(t *testing.T) {
 80 | 	// Setup workflow with specific queue
 81 | 	workflow := config.WorkflowDef{
 82 | 		TaskQueue: "specific-queue",
 83 | 	}
 84 | 
 85 | 	// Setup config with default queue
 86 | 	cfg := &config.Config{
 87 | 		Temporal: config.TemporalConfig{
 88 | 			DefaultTaskQueue: "default-queue",
 89 | 		},
 90 | 	}
 91 | 
 92 | 	// Check that workflow queue takes precedence
 93 | 	resultQueue := workflow.TaskQueue
 94 | 	if resultQueue != "specific-queue" {
 95 | 		t.Errorf("Workflow queue should be 'specific-queue', got '%s'", resultQueue)
 96 | 	}
 97 | 
 98 | 	// Verify it doesn't use default queue when workflow has one
 99 | 	if workflow.TaskQueue == "" && cfg.Temporal.DefaultTaskQueue != "" {
100 | 		t.Error("Test condition error: Should not use default when workflow queue exists")
101 | 	}
102 | }
103 | 
104 | // TestDefaultTaskQueueFallback ensures default task queue is used as fallback
105 | func TestDefaultTaskQueueFallback(t *testing.T) {
106 | 	// Setup workflow without specific queue
107 | 	workflow := config.WorkflowDef{
108 | 		TaskQueue: "", // No task queue specified
109 | 	}
110 | 
111 | 	// Setup config with default queue
112 | 	cfg := &config.Config{
113 | 		Temporal: config.TemporalConfig{
114 | 			DefaultTaskQueue: "default-queue",
115 | 		},
116 | 	}
117 | 
118 | 	// Get the task queue that would be used
119 | 	taskQueue := workflow.TaskQueue
120 | 	if taskQueue == "" {
121 | 		taskQueue = cfg.Temporal.DefaultTaskQueue
122 | 	}
123 | 
124 | 	// Verify default queue is used
125 | 	if taskQueue != "default-queue" {
126 | 		t.Errorf("Should use default queue when workflow queue is empty, got '%s'", taskQueue)
127 | 	}
128 | 
129 | 	// Verify workflow queue is actually empty
130 | 	if workflow.TaskQueue != "" {
131 | 		t.Errorf("Workflow queue should be empty, got '%s'", workflow.TaskQueue)
132 | 	}
133 | 
134 | 	// Verify default queue is correctly set
135 | 	if cfg.Temporal.DefaultTaskQueue != "default-queue" {
136 | 		t.Errorf("Default queue should be 'default-queue', got '%s'", cfg.Temporal.DefaultTaskQueue)
137 | 	}
138 | }
139 | 
140 | // TestWorkflowInputParams tests that workflow inputs are correctly passed to ExecuteWorkflow
141 | func TestWorkflowInputParams(t *testing.T) {
142 | 	// Define test cases for different workflow input types
143 | 	type TestWorkflowRequest struct {
144 | 		ID   string `json:"id"`
145 | 		Name string `json:"name"`
146 | 		Data string `json:"data"`
147 | 	}
148 | 
149 | 	// Mock workflow parameters
150 | 	testCases := []struct {
151 | 		name       string
152 | 		workflowID string
153 | 		params     interface{}
154 | 	}{
155 | 		{
156 | 			name:       "Basic string parameter",
157 | 			workflowID: "string-param-workflow",
158 | 			params:     "simple-string-value",
159 | 		},
160 | 		{
161 | 			name:       "Struct parameter",
162 | 			workflowID: "struct-param-workflow",
163 | 			params: TestWorkflowRequest{
164 | 				ID:   "test-123",
165 | 				Name: "Test Workflow",
166 | 				Data: "Sample payload data",
167 | 			},
168 | 		},
169 | 		{
170 | 			name:       "Map parameter",
171 | 			workflowID: "map-param-workflow",
172 | 			params: map[string]interface{}{
173 | 				"id":      "map-123",
174 | 				"enabled": true,
175 | 				"count":   42,
176 | 				"nested": map[string]string{
177 | 					"key": "value",
178 | 				},
179 | 			},
180 | 		},
181 | 	}
182 | 
183 | 	for _, tc := range testCases {
184 | 		t.Run(tc.name, func(t *testing.T) {
185 | 			// Recreate the workflow execution context
186 | 			ctx := context.Background()
187 | 
188 | 			// Verify parameters are correctly structured for ExecuteWorkflow
189 | 			// We can't directly test the execution but we can verify the parameters are correct
190 | 			switch params := tc.params.(type) {
191 | 			case string:
192 | 				if params == "" {
193 | 					t.Error("String parameter should not be empty")
194 | 				}
195 | 			case TestWorkflowRequest:
196 | 				if params.ID == "" {
197 | 					t.Error("Request ID should not be empty")
198 | 				}
199 | 				if params.Name == "" {
200 | 					t.Error("Request Name should not be empty")
201 | 				}
202 | 			case map[string]interface{}:
203 | 				if id, ok := params["id"]; !ok || id == "" {
204 | 					t.Error("Map parameter should have non-empty 'id' field")
205 | 				}
206 | 				if nested, ok := params["nested"].(map[string]string); !ok {
207 | 					t.Error("Map parameter should have valid nested map")
208 | 				} else if _, ok := nested["key"]; !ok {
209 | 					t.Error("Nested map should have 'key' property")
210 | 				}
211 | 			default:
212 | 				t.Errorf("Unexpected parameter type: %T", tc.params)
213 | 			}
214 | 
215 | 			// Verify context is valid
216 | 			if ctx == nil {
217 | 				t.Error("Context should not be nil")
218 | 			}
219 | 		})
220 | 	}
221 | }
222 | 
223 | func TestWorkflowIDComputation(t *testing.T) {
224 | 	type Case struct {
225 | 		recipe   string
226 | 		args     map[string]string
227 | 		expected string
228 | 	}
229 | 
230 | 	tests := map[string]Case{
231 | 		"empty": {
232 | 			recipe:   "",
233 | 			expected: "",
234 | 		},
235 | 		"reference args": {
236 | 			recipe:   "id_{{ .one }}_{{ .two }}",
237 | 			args:     map[string]string{"one": "1", "two": "2"},
238 | 			expected: "id_1_2",
239 | 		},
240 | 		"reference missing args": {
241 | 			recipe:   "id_{{ .one }}_{{ .missing }}",
242 | 			args:     map[string]string{"one": "1"},
243 | 			expected: "id_1_<no value>",
244 | 		},
245 | 		"hash all args by accident": {
246 | 			recipe:   "id_{{ hash }}",
247 | 			args:     map[string]string{"one": "1", "two": "2"},
248 | 			expected: "id_3822076040",
249 | 		},
250 | 		"hash all args properly": {
251 | 			recipe:   "id_{{ hash . }}",
252 | 			args:     map[string]string{"one": "1", "two": "2"},
253 | 			expected: "id_3822076040",
254 | 		},
255 | 		"hash some args": {
256 | 			recipe:   "id_{{ hash .one .two }}",
257 | 			args:     map[string]string{"one": "1", "two": "2"},
258 | 			expected: "id_1475351198",
259 | 		},
260 | 	}
261 | 	for name, tc := range tests {
262 | 		t.Run(name, func(t *testing.T) {
263 | 			def := config.WorkflowDef{
264 | 				WorkflowIDRecipe: tc.recipe,
265 | 			}
266 | 			actual, err := computeWorkflowID(def, tc.args)
267 | 			require.NoError(t, err)
268 | 			require.Equal(t, tc.expected, actual)
269 | 		})
270 | 	}
271 | }
272 | 
```
--------------------------------------------------------------------------------
/internal/temporal/client_test.go:
--------------------------------------------------------------------------------
```go
  1 | package temporal
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"strings"
  6 | 	"testing"
  7 | 	"time"
  8 | 
  9 | 	"github.com/mocksi/temporal-mcp/internal/config"
 10 | 	"go.temporal.io/sdk/client"
 11 | )
 12 | 
 13 | // TestNewTemporalClient tests the client creation with different configurations
 14 | func TestNewTemporalClient(t *testing.T) {
 15 | 	// Test valid local configuration
 16 | 	t.Run("ValidLocalConfig", func(t *testing.T) {
 17 | 		// Use a non-standard port to ensure we won't accidentally connect to a real server
 18 | 		cfg := config.TemporalConfig{
 19 | 			HostPort:    "localhost:12345", // Use a port that's unlikely to have a Temporal server
 20 | 			Namespace:   "default",
 21 | 			Environment: "local",
 22 | 			Timeout:     "5s",
 23 | 		}
 24 | 
 25 | 		// Attempt to create client - we expect a connection error, not a config error
 26 | 		client, err := NewTemporalClient(cfg)
 27 | 
 28 | 		// Check that either:
 29 | 		// 1. We got a connection error (most likely case)
 30 | 		// 2. Or somehow we got a valid client (unlikely, but possible if a test server is running)
 31 | 		if err != nil {
 32 | 			// Verify this is a connection error, not a config validation error
 33 | 			if !strings.Contains(err.Error(), "failed to create Temporal client") {
 34 | 				t.Errorf("Expected connection error, got: %v", err)
 35 | 			}
 36 | 		} else {
 37 | 			// If we got a client, make sure to close it
 38 | 			defer client.Close()
 39 | 		}
 40 | 	})
 41 | 
 42 | 	// Test invalid environment
 43 | 	t.Run("InvalidEnvironment", func(t *testing.T) {
 44 | 		cfg := config.TemporalConfig{
 45 | 			HostPort:    "localhost:7233",
 46 | 			Namespace:   "default",
 47 | 			Environment: "invalid",
 48 | 			Timeout:     "5s",
 49 | 		}
 50 | 
 51 | 		_, err := NewTemporalClient(cfg)
 52 | 		if err == nil {
 53 | 			t.Error("Expected error for invalid environment, got nil")
 54 | 		}
 55 | 	})
 56 | 
 57 | 	// Test invalid timeout
 58 | 	t.Run("InvalidTimeout", func(t *testing.T) {
 59 | 		cfg := config.TemporalConfig{
 60 | 			HostPort:    "localhost:7233",
 61 | 			Namespace:   "default",
 62 | 			Environment: "local",
 63 | 			Timeout:     "invalid",
 64 | 		}
 65 | 
 66 | 		_, err := NewTemporalClient(cfg)
 67 | 		if err == nil {
 68 | 			t.Error("Expected error for invalid timeout, got nil")
 69 | 		}
 70 | 	})
 71 | 
 72 | 	// Test remote environment (which is not implemented yet)
 73 | 	t.Run("RemoteEnvironment", func(t *testing.T) {
 74 | 		cfg := config.TemporalConfig{
 75 | 			HostPort:    "test.tmprl.cloud:7233",
 76 | 			Namespace:   "test-namespace",
 77 | 			Environment: "remote",
 78 | 		}
 79 | 
 80 | 		_, err := NewTemporalClient(cfg)
 81 | 		if err == nil {
 82 | 			t.Error("Expected error for unimplemented remote environment, got nil")
 83 | 		}
 84 | 	})
 85 | }
 86 | 
 87 | // MockWorkflowClient is a mock implementation of the Temporal client for testing
 88 | type MockWorkflowClient struct {
 89 | 	lastWorkflowName string
 90 | 	lastParams       interface{}
 91 | 	lastOptions      client.StartWorkflowOptions
 92 | }
 93 | 
 94 | // ExecuteWorkflow mocks the ExecuteWorkflow method for testing
 95 | func (m *MockWorkflowClient) ExecuteWorkflow(ctx context.Context, options client.StartWorkflowOptions, workflow interface{}, args ...interface{}) (client.WorkflowRun, error) {
 96 | 	m.lastWorkflowName = workflow.(string)
 97 | 	m.lastOptions = options
 98 | 	if len(args) > 0 {
 99 | 		m.lastParams = args[0]
100 | 	}
101 | 	// Return a mock workflow run
102 | 	return &MockWorkflowRun{}, nil
103 | }
104 | 
105 | // Close is a no-op for the mock client
106 | func (m *MockWorkflowClient) Close() {}
107 | 
108 | // MockWorkflowRun is a mock implementation of WorkflowRun for testing
109 | type MockWorkflowRun struct{}
110 | 
111 | // GetID returns a mock workflow ID
112 | func (m *MockWorkflowRun) GetID() string {
113 | 	return "mock-workflow-id"
114 | }
115 | 
116 | // GetRunID returns a mock run ID
117 | func (m *MockWorkflowRun) GetRunID() string {
118 | 	return "mock-run-id"
119 | }
120 | 
121 | // Get is a mock implementation that returns no error
122 | func (m *MockWorkflowRun) Get(ctx context.Context, valuePtr interface{}) error {
123 | 	return nil
124 | }
125 | 
126 | // GetWithOptions is a mock implementation of the WorkflowRun interface method
127 | func (m *MockWorkflowRun) GetWithOptions(ctx context.Context, valuePtr interface{}, opts client.WorkflowRunGetOptions) error {
128 | 	return nil
129 | }
130 | 
131 | // TestWorkflowExecution tests workflow execution with different types of input parameters
132 | func TestWorkflowExecution(t *testing.T) {
133 | 	// Define test structs
134 | 	type TestRequest struct {
135 | 		ID    string `json:"id"`
136 | 		Value string `json:"value"`
137 | 	}
138 | 
139 | 	type ComplexRequest struct {
140 | 		ClientID  string                 `json:"client_id"`
141 | 		Command   string                 `json:"command"`
142 | 		Data      map[string]interface{} `json:"data"`
143 | 		Timestamp time.Time              `json:"timestamp"`
144 | 	}
145 | 
146 | 	// Test cases with different input types
147 | 	testCases := []struct {
148 | 		name           string
149 | 		workflowName   string
150 | 		taskQueue      string
151 | 		params         interface{}
152 | 		expectedParams interface{}
153 | 	}{
154 | 		{
155 | 			name:           "String Parameter",
156 | 			workflowName:   "string-workflow",
157 | 			taskQueue:      "default-queue",
158 | 			params:         "simple-string-input",
159 | 			expectedParams: "simple-string-input",
160 | 		},
161 | 		{
162 | 			name:         "Struct Parameter",
163 | 			workflowName: "struct-workflow",
164 | 			taskQueue:    "test-queue",
165 | 			params: TestRequest{
166 | 				ID:    "req-123",
167 | 				Value: "test-value",
168 | 			},
169 | 			expectedParams: TestRequest{
170 | 				ID:    "req-123",
171 | 				Value: "test-value",
172 | 			},
173 | 		},
174 | 		{
175 | 			name:         "Complex Parameter",
176 | 			workflowName: "complex-workflow",
177 | 			taskQueue:    "complex-queue",
178 | 			params: ComplexRequest{
179 | 				ClientID:  "client-456",
180 | 				Command:   "analyze",
181 | 				Data:      map[string]interface{}{"key": "value"},
182 | 				Timestamp: time.Now(),
183 | 			},
184 | 			expectedParams: ComplexRequest{
185 | 				ClientID: "client-456",
186 | 				Command:  "analyze",
187 | 				Data:     map[string]interface{}{"key": "value"},
188 | 				// Time will be different but type should match
189 | 			},
190 | 		},
191 | 		{
192 | 			name:         "Map Parameter",
193 | 			workflowName: "map-workflow",
194 | 			taskQueue:    "map-queue",
195 | 			params: map[string]interface{}{
196 | 				"id":     "map-789",
197 | 				"count":  42,
198 | 				"active": true,
199 | 			},
200 | 			expectedParams: map[string]interface{}{
201 | 				"id":     "map-789",
202 | 				"count":  42,
203 | 				"active": true,
204 | 			},
205 | 		},
206 | 	}
207 | 
208 | 	for _, tc := range testCases {
209 | 		t.Run(tc.name, func(t *testing.T) {
210 | 			// Create a mock client
211 | 			mockClient := &MockWorkflowClient{}
212 | 
213 | 			// Execute the workflow with the test parameters
214 | 			ctx := context.Background()
215 | 			options := client.StartWorkflowOptions{
216 | 				ID:        "test-" + tc.workflowName,
217 | 				TaskQueue: tc.taskQueue,
218 | 			}
219 | 
220 | 			// Call ExecuteWorkflow on the mock client
221 | 			_, err := mockClient.ExecuteWorkflow(ctx, options, tc.workflowName, tc.params)
222 | 			if err != nil {
223 | 				t.Fatalf("ExecuteWorkflow failed: %v", err)
224 | 			}
225 | 
226 | 			// Verify workflow name
227 | 			if mockClient.lastWorkflowName != tc.workflowName {
228 | 				t.Errorf("Expected workflow name %s, got %s", tc.workflowName, mockClient.lastWorkflowName)
229 | 			}
230 | 
231 | 			// Verify task queue
232 | 			if mockClient.lastOptions.TaskQueue != tc.taskQueue {
233 | 				t.Errorf("Expected task queue %s, got %s", tc.taskQueue, mockClient.lastOptions.TaskQueue)
234 | 			}
235 | 
236 | 			// Verify parameters were passed correctly
237 | 			switch params := mockClient.lastParams.(type) {
238 | 			case string:
239 | 				expectedStr, ok := tc.expectedParams.(string)
240 | 				if !ok || params != expectedStr {
241 | 					t.Errorf("Expected string param %v, got %v", tc.expectedParams, params)
242 | 				}
243 | 			case TestRequest:
244 | 				expected, ok := tc.expectedParams.(TestRequest)
245 | 				if !ok || params.ID != expected.ID || params.Value != expected.Value {
246 | 					t.Errorf("Expected struct param %v, got %v", tc.expectedParams, params)
247 | 				}
248 | 			case ComplexRequest:
249 | 				expected, ok := tc.expectedParams.(ComplexRequest)
250 | 				if !ok || params.ClientID != expected.ClientID || params.Command != expected.Command {
251 | 					t.Errorf("Expected complex param %v, got %v", tc.expectedParams, params)
252 | 				}
253 | 			case map[string]interface{}:
254 | 				expected, ok := tc.expectedParams.(map[string]interface{})
255 | 				if !ok {
256 | 					t.Errorf("Expected map param %v, got %v", tc.expectedParams, params)
257 | 				}
258 | 				// Check key values
259 | 				for k, v := range expected {
260 | 					if params[k] != v {
261 | 						t.Errorf("Expected map[%s]=%v, got %v", k, v, params[k])
262 | 					}
263 | 				}
264 | 			default:
265 | 				t.Errorf("Unexpected parameter type: %T", params)
266 | 			}
267 | 		})
268 | 	}
269 | }
270 | 
```
--------------------------------------------------------------------------------
/docs/temporal.md:
--------------------------------------------------------------------------------
```markdown
  1 | # Starting and Getting Responses from Temporal Workflows in Go: A Developer's Guide
  2 | 
  3 | This guide provides a practical pathway for Go developers to effectively start Temporal workflow executions and retrieve their responses, covering essential concepts and implementation steps.
  4 | 
  5 | ## Introduction to Temporal
  6 | 
  7 | Temporal is a distributed, scalable orchestration engine that helps you build and run reliable workflows for your applications. It handles state persistence, automatic retries, and complex coordination logic between services[2]. The platform consists of a programming framework (client library) and a managed service (backend)[2].
  8 | 
  9 | ## Setting Up Your Environment
 10 | 
 11 | ### Installing the Temporal Go SDK
 12 | 
 13 | ```bash
 14 | go get go.temporal.io/sdk
 15 | ```
 16 | 
 17 | ### Connecting to Temporal Service
 18 | 
 19 | ```go
 20 | import (
 21 |     "go.temporal.io/sdk/client"
 22 | )
 23 | 
 24 | // Create a Temporal Client to communicate with the Temporal Service
 25 | temporalClient, err := client.Dial(client.Options{
 26 |     HostPort: client.DefaultHostPort, // Defaults to "127.0.0.1:7233"
 27 | })
 28 | if err != nil {
 29 |     log.Fatalln("Unable to create Temporal Client", err)
 30 | }
 31 | defer temporalClient.Close()
 32 | ```
 33 | 
 34 | For Temporal Cloud connections:
 35 | ```go
 36 | // For Temporal Cloud
 37 | temporalClient, err := client.Dial(client.Options{
 38 |     HostPort:  "your-namespace.tmprl.cloud:7233",
 39 |     Namespace: "your-namespace",
 40 |     ConnectionOptions: client.ConnectionOptions{
 41 |         TLS: &tls.Config{},
 42 |     },
 43 | })
 44 | ```
 45 | 
 46 | ## Defining a Simple Workflow
 47 | 
 48 | ```go
 49 | import (
 50 |     "time"
 51 |     "go.temporal.io/sdk/workflow"
 52 | )
 53 | 
 54 | // Define your workflow function
 55 | func GreetingWorkflow(ctx workflow.Context, name string) (string, error) {
 56 |     // Set activity options
 57 |     ao := workflow.ActivityOptions{
 58 |         TaskQueue:              "greeting-tasks",
 59 |         StartToCloseTimeout:    time.Minute,
 60 |         ScheduleToCloseTimeout: time.Minute,
 61 |     }
 62 |     ctx = workflow.WithActivityOptions(ctx, ao)
 63 |     
 64 |     // Execute activity and get result
 65 |     var result string
 66 |     err := workflow.ExecuteActivity(ctx, GreetingActivity, name).Get(ctx, &result)
 67 |     if err != nil {
 68 |         return "", err
 69 |     }
 70 |     
 71 |     return result, nil
 72 | }
 73 | 
 74 | // Define your activity function
 75 | func GreetingActivity(ctx context.Context, name string) (string, error) {
 76 |     return "Hello, " + name + "!", nil
 77 | }
 78 | ```
 79 | 
 80 | ## Creating a Worker
 81 | 
 82 | ```go
 83 | import (
 84 |     "go.temporal.io/sdk/worker"
 85 | )
 86 | 
 87 | func startWorker(c client.Client) {
 88 |     // Create worker options
 89 |     w := worker.New(c, "greeting-tasks", worker.Options{})
 90 |     
 91 |     // Register workflow and activity with the worker
 92 |     w.RegisterWorkflow(GreetingWorkflow)
 93 |     w.RegisterActivity(GreetingActivity)
 94 |     
 95 |     // Start the worker
 96 |     err := w.Run(worker.InterruptCh())
 97 |     if err != nil {
 98 |         log.Fatalln("Unable to start worker", err)
 99 |     }
100 | }
101 | ```
102 | 
103 | ## Starting Workflow Executions
104 | 
105 | ```go
106 | // Define workflow options
107 | workflowOptions := client.StartWorkflowOptions{
108 |     ID:        "greeting-workflow-" + uuid.New().String(),
109 |     TaskQueue: "greeting-tasks",
110 | }
111 | 
112 | // Start the workflow execution
113 | workflowRun, err := temporalClient.ExecuteWorkflow(
114 |     context.Background(), 
115 |     workflowOptions, 
116 |     GreetingWorkflow, 
117 |     "Temporal Developer"
118 | )
119 | if err != nil {
120 |     log.Fatalln("Unable to execute workflow", err)
121 | }
122 | 
123 | // Get workflow ID and run ID for future reference
124 | fmt.Printf("Started workflow: WorkflowID: %s, RunID: %s\n", 
125 |     workflowRun.GetID(), 
126 |     workflowRun.GetRunID())
127 | ```
128 | 
129 | ## Getting Responses from Workflow Executions
130 | 
131 | ### 1. Synchronous Response
132 | 
133 | ```go
134 | // Wait for workflow completion and get result
135 | var result string
136 | err = workflowRun.Get(context.Background(), &result)
137 | if err != nil {
138 |     log.Fatalln("Unable to get workflow result", err)
139 | }
140 | fmt.Printf("Workflow result: %s\n", result)
141 | ```
142 | 
143 | ### 2. Retrieving Results Later
144 | 
145 | ```go
146 | // Get workflow result using workflow ID and run ID
147 | workflowID := "greeting-workflow-123"
148 | runID := "run-id-456"
149 | 
150 | // Retrieve the workflow handle
151 | workflowRun = temporalClient.GetWorkflow(context.Background(), workflowID, runID)
152 | 
153 | // Get the result
154 | var result string
155 | err = workflowRun.Get(context.Background(), &result)
156 | if err != nil {
157 |     log.Fatalln("Unable to get workflow result", err)
158 | }
159 | ```
160 | 
161 | ### 3. Using Queries to Get Workflow State
162 | 
163 | ```go
164 | // Define query handler in your workflow
165 | func GreetingWorkflow(ctx workflow.Context, name string) (string, error) {
166 |     // Set up state variable
167 |     greeting := ""
168 |     
169 |     // Register query handler
170 |     err := workflow.SetQueryHandler(ctx, "getGreeting", func() (string, error) {
171 |         return greeting, nil
172 |     })
173 |     if err != nil {
174 |         return "", err
175 |     }
176 |     
177 |     // Workflow logic...
178 |     greeting = "Hello, " + name + "!"
179 |     
180 |     return greeting, nil
181 | }
182 | 
183 | // Query the workflow state from client
184 | response, err := temporalClient.QueryWorkflow(context.Background(), 
185 |     workflowID, runID, "getGreeting")
186 | if err != nil {
187 |     log.Fatalln("Unable to query workflow", err)
188 | }
189 | 
190 | var greeting string
191 | err = response.Get(&greeting)
192 | if err != nil {
193 |     log.Fatalln("Unable to decode query result", err)
194 | }
195 | fmt.Printf("Current greeting: %s\n", greeting)
196 | ```
197 | 
198 | ### 4. Message Passing with Signals
199 | 
200 | ```go
201 | // In your workflow, set up a signal channel
202 | func GreetingWorkflow(ctx workflow.Context, name string) (string, error) {
203 |     // Create signal channel
204 |     updateNameChannel := workflow.GetSignalChannel(ctx, "update_name")
205 |     
206 |     for {
207 |         // Wait for signal or timeout
208 |         selector := workflow.NewSelector(ctx)
209 |         selector.AddReceive(updateNameChannel, func(c workflow.ReceiveChannel, more bool) {
210 |             var newName string
211 |             c.Receive(ctx, &newName)
212 |             name = newName
213 |             // Process updated name...
214 |         })
215 |         
216 |         // Add timeout to exit workflow
217 |         selector.Select(ctx)
218 |     }
219 | }
220 | 
221 | // Send signal to workflow
222 | err = temporalClient.SignalWorkflow(context.Background(), 
223 |     workflowID, runID, "update_name", "New Name")
224 | if err != nil {
225 |     log.Fatalln("Unable to signal workflow", err)
226 | }
227 | ```
228 | 
229 | ## Error Handling and Retries
230 | 
231 | ```go
232 | // Configure retry policy
233 | retryPolicy := &temporal.RetryPolicy{
234 |     InitialInterval:    time.Second,
235 |     BackoffCoefficient: 2.0,
236 |     MaximumInterval:    time.Minute * 5,
237 |     MaximumAttempts:    5,
238 | }
239 | 
240 | // Apply retry policy to activity options
241 | ao := workflow.ActivityOptions{
242 |     TaskQueue:              "greeting-tasks",
243 |     StartToCloseTimeout:    time.Minute,
244 |     ScheduleToCloseTimeout: time.Minute,
245 |     RetryPolicy:            retryPolicy,
246 | }
247 | ```
248 | 
249 | ## Getting Workflow Information
250 | 
251 | ```go
252 | // Inside a workflow, get workflow execution info
253 | info := workflow.GetInfo(ctx)
254 | workflowID := info.WorkflowExecution.ID
255 | runID := info.WorkflowExecution.RunID[7]
256 | ```
257 | 
258 | ## Workflow Run ID
259 | 
260 | To get the current run ID within a workflow (useful for self-termination)[7]:
261 | ```go
262 | // Inside a workflow
263 | runID := workflow.GetInfo(ctx).WorkflowExecution.RunID
264 | ```
265 | 
266 | ## Conclusion
267 | 
268 | This guide provides the essential steps to start and get responses from Temporal workflow executions in Go. Temporal offers a powerful framework for building reliable, distributed applications with durable execution state. For more advanced features, refer to the official Temporal documentation and explore the sample applications[12].
269 | 
270 | Remember that Temporal is particularly valuable for scenarios involving:
271 | - Long-running, potentially multi-step processes
272 | - Coordination between multiple services
273 | - Processes requiring automatic retries
274 | - Workflows that need to maintain state even through system failures[14]
275 | 
276 | By leveraging Temporal's fault-tolerance capabilities, you can build applications that reliably execute complex business logic while focusing on your business requirements rather than infrastructure concerns.
277 | 
278 | Sources
279 | [1] Go SDK developer guide | Temporal Platform Documentation https://docs.temporal.io/develop/go
280 | [2] temporal - Go Packages https://pkg.go.dev/go.temporal.io/sdk/temporal
281 | [3] Workflow message passing - Go SDK - Temporal Docs https://docs.temporal.io/develop/go/message-passing
282 | [4] temporalio/sdk-go: Temporal Go SDK - GitHub https://github.com/temporalio/sdk-go
283 | [5] Temporal Client - Go SDK https://docs.temporal.io/develop/go/temporal-clients
284 | [6] README.md - Temporal Go SDK samples - GitHub https://github.com/temporalio/samples-go/blob/main/README.md
285 | [7] Temporal, How to get RunID while being inside a workflow to ... https://stackoverflow.com/questions/73229921/temporal-how-to-get-runid-while-being-inside-a-workflow-to-terminate-the-curren
286 | [8] Run your first Temporal application with the Go SDK https://learn.temporal.io/getting_started/go/first_program_in_go/
287 | [9] Go SDK developer guide | Temporal Platform Documentation https://docs.temporal.io/develop/go/
288 | [10] Build a Temporal Application from scratch in Go https://learn.temporal.io/getting_started/go/hello_world_in_go/
289 | [11] workflow package - go.temporal.io/sdk/workflow - Go Packages https://pkg.go.dev/go.temporal.io/sdk/workflow
290 | [12] temporalio/samples-go: Temporal Go SDK samples - GitHub https://github.com/temporalio/samples-go
291 | [13] workflow - Go Packages https://pkg.go.dev/go.temporal.io/temporal/workflow
292 | [14] When to use a Workflow tool (Temporal) vs a Job Queue - Reddit https://www.reddit.com/r/golang/comments/1as23yb/when_to_use_a_workflow_tool_temporal_vs_a_job/
293 | [15] workflowcheck command - go.temporal.io/sdk/contrib/tools ... https://pkg.go.dev/go.temporal.io/sdk/contrib/tools/workflowcheck
294 | [16] Implementing Temporal IO in Golang Microservices Architecture https://www.softwareletters.com/p/implementing-temporal-io-golang-microservices-architecture-stepbystep-guide
295 | [17] Using Temporal and Go SDK for flows orchestration : r/golang - Reddit https://www.reddit.com/r/golang/comments/1dy2np1/using_temporal_and_go_sdk_for_flows_orchestration/
296 | [18] Temporal Workflow | Temporal Platform Documentation https://docs.temporal.io/workflows
297 | [19] Intro to Temporal with Go SDK - YouTube https://www.youtube.com/watch?v=-KWutSkFda8
298 | [20] Temporal SDK : r/golang - Reddit https://www.reddit.com/r/golang/comments/15kwzke/temporal_sdk/
299 | [21] Core application - Go SDK | Temporal Platform Documentation https://docs.temporal.io/develop/go/core-application
300 | [22] Get started with Temporal and Go https://learn.temporal.io/getting_started/go/
301 | [23] temporal: when testing, how do I pass context into workflows and ... https://stackoverflow.com/questions/69577516/temporal-when-testing-how-do-i-pass-context-into-workflows-and-activities
302 | [24] Workflow with Temporal - Capten.AI https://capten.ai/learning-center/10-learn-temporal/understand-temporal-workflow/workflow/
303 | [25] client package - go.temporal.io/sdk/client - Go Packages https://pkg.go.dev/go.temporal.io/sdk/client
304 | [26] documentation-samples-go/yourapp/your_workflow_definition_dacx ... https://github.com/temporalio/documentation-samples-go/blob/main/yourapp/your_workflow_definition_dacx.go
305 | 
```
--------------------------------------------------------------------------------
/docs/VERSION_0.md:
--------------------------------------------------------------------------------
```markdown
  1 | # Temporal MCP Technical Specification
  2 | 
  3 | ## 1. Introduction
  4 | 
  5 | ### Purpose and Goals
  6 | The primary purpose of this project is to create a Model Control Protocol (MCP) server implementation in Go that:
  7 | - Reads a YAML configuration defining Temporal workflows
  8 | - Automatically exposes each workflow as an MCP tool
  9 | - Handles tool invocation requests from MCP clients
 10 | - Executes workflows via Temporal and returns results
 11 | 
 12 | ### Scope of Implementation
 13 | This specification covers the development of an MCP-compliant server that:
 14 | - Parses workflow definitions from YAML configuration
 15 | - Connects to a Temporal service using the Go SDK
 16 | - Dynamically generates MCP tool definitions based on the configured workflows
 17 | - Handles tool invocation lifecycle and result retrieval
 18 | - Implements caching with cache invalidation capabilities
 19 | 
 20 | ### Definitions and Acronyms
 21 | - **MCP**: Model Control Protocol - A protocol for communication between AI models and external tools
 22 | - **Temporal**: A distributed, scalable workflow orchestration engine
 23 | - **Workflow**: A durable function that orchestrates activities in Temporal
 24 | - **YAML**: YAML Ain't Markup Language - A human-friendly data serialization standard
 25 | 
 26 | ## 2. System Overview
 27 | 
 28 | ### Client-Host-Server Architecture
 29 | ```
 30 | +----------------+     +----------------+     +----------------+
 31 | |  MCP Client    |<--->|  MCP Server    |<--->|  Temporal      |
 32 | | (AI Assistant) |     | (Go Service)   |     |  Service       |
 33 | +----------------+     +----------------+     +----------------+
 34 |                           ^       ^
 35 |                           |       |
 36 |                           v       v
 37 |                     +----------------+     +----------------+
 38 |                     | YAML Workflow  |     | SQLite Cache  |
 39 |                     | Configuration  |     | Database      |
 40 |                     +----------------+     +----------------+
 41 | ```
 42 | 
 43 | ### Server Components
 44 | 1. **Config Parser**: Loads and parses YAML workflow definitions
 45 | 2. **MCP Protocol Handler**: Manages MCP message processing
 46 | 3. **Temporal Client**: Interfaces with Temporal service
 47 | 4. **Tool Registry**: Dynamically generates tool definitions from workflows
 48 | 5. **Cache Manager**: Handles SQLite caching for workflow results
 49 | 
 50 | ## 3. MCP Protocol Implementation
 51 | 
 52 | ### 3.1 Protocol Definition
 53 | - Compliant with MCP Specification v0.1
 54 | - JSON-based message exchange
 55 | - Request/response communication pattern
 56 | 
 57 | ### 3.2 Message Format
 58 | 
 59 | #### Tool Definitions Format
 60 | Tools are dynamically generated from workflow definitions in the YAML configuration, with an additional tool for cache management.
 61 | 
 62 | ## 4. Server Implementation
 63 | 
 64 | ### Core Components
 65 | 
 66 | #### YAML Configuration Schema
 67 | ```go
 68 | type Config struct {
 69 |     Temporal  TemporalConfig           `yaml:"temporal"`
 70 |     Workflows map[string]WorkflowDef   `yaml:"workflows"`
 71 |     Cache     CacheConfig              `yaml:"cache"`
 72 | }
 73 | 
 74 | type TemporalConfig struct {
 75 |     // Connection configuration
 76 |     HostPort  string    `yaml:"hostPort"`
 77 |     Namespace string    `yaml:"namespace"`
 78 | 
 79 |     // Environment type
 80 |     Environment string  `yaml:"environment"` // "local" or "remote"
 81 | 
 82 |     // Authentication (for remote)
 83 |     Auth      *AuthConfig `yaml:"auth,omitempty"`
 84 | 
 85 |     // TLS configuration (for remote)
 86 |     TLS       *TLSConfig `yaml:"tls,omitempty"`
 87 | 
 88 |     // Connection options
 89 |     RetryOptions *RetryConfig `yaml:"retryOptions,omitempty"`
 90 |     Timeout      string       `yaml:"timeout,omitempty"`
 91 | }
 92 | 
 93 | type CacheConfig struct {
 94 |     Enabled        bool   `yaml:"enabled"`
 95 |     DatabasePath   string `yaml:"databasePath"`
 96 |     TTL            string `yaml:"ttl"`             // Time-to-live for cached results
 97 |     MaxCacheSize   int64  `yaml:"maxCacheSize"`    // Maximum size in bytes
 98 |     CleanupInterval string `yaml:"cleanupInterval"` // How often to clean expired entries
 99 | }
100 | ```
101 | 
102 | ## 5. Cache Implementation
103 | 
104 | ### Cache Clear Tool
105 | The server implements a special tool for cache management:
106 | 
107 | ```json
108 | {
109 |   "name": "ClearCache",
110 |   "description": "Clears cached workflow results, either by specific workflow or the entire cache.",
111 |   "parameters": {
112 |     "type": "object",
113 |     "properties": {
114 |       "workflowName": {
115 |         "type": "string",
116 |         "description": "Optional. Name of the workflow to clear the cache for. If not provided, all cache entries will be cleared."
117 |       }
118 |     },
119 |     "required": []
120 |   }
121 | }
122 | ```
123 | 
124 | ### Cache Manager with Clear Function
125 | ```go
126 | func (cm *CacheManager) Clear(workflowName string) (int64, error) {
127 |     if !cm.enabled {
128 |         return 0, nil
129 |     }
130 | 
131 |     var result sql.Result
132 |     var err error
133 | 
134 |     if workflowName == "" {
135 |         // Clear entire cache
136 |         result, err = cm.db.Exec("DELETE FROM workflow_cache")
137 |     } else {
138 |         // Clear cache for specific workflow
139 |         result, err = cm.db.Exec(
140 |             "DELETE FROM workflow_cache WHERE workflow_name = ?",
141 |             workflowName,
142 |         )
143 |     }
144 | 
145 |     if err != nil {
146 |         return 0, fmt.Errorf("failed to clear cache: %w", err)
147 |     }
148 | 
149 |     rowsAffected, err := result.RowsAffected()
150 |     if err != nil {
151 |         return 0, fmt.Errorf("failed to get rows affected: %w", err)
152 |     }
153 | 
154 |     return rowsAffected, nil
155 | }
156 | ```
157 | 
158 | ### Clear Cache Tool Handler
159 | ```go
160 | func (s *MCPServer) handleClearCache(params map[string]interface{}) (ToolCallResponse, error) {
161 |     // Extract workflow name if provided
162 |     var workflowName string
163 |     if name, ok := params["workflowName"].(string); ok {
164 |         workflowName = name
165 |     }
166 | 
167 |     // Clear cache
168 |     rowsAffected, err := s.cacheManager.Clear(workflowName)
169 |     if err != nil {
170 |         return ToolCallResponse{}, fmt.Errorf("failed to clear cache: %w", err)
171 |     }
172 | 
173 |     // Create response
174 |     response := ToolCallResponse{
175 |         ToolName: "ClearCache",
176 |         Status:   "completed",
177 |         Result: map[string]interface{}{
178 |             "success":      true,
179 |             "entriesCleared": rowsAffected,
180 |             "workflow":     workflowName,
181 |         },
182 |     }
183 | 
184 |     return response, nil
185 | }
186 | ```
187 | 
188 | ### Tool Registration Integration
189 | ```go
190 | func (s *MCPServer) generateToolDefinitions() error {
191 |     // Generate tools for workflows
192 |     for name, workflow := range s.config.Workflows {
193 |         // Create tool definition for workflow
194 |         // ...existing code...
195 |     }
196 | 
197 |     // Add special tool for clearing cache
198 |     s.tools["ClearCache"] = ToolDefinition{
199 |         Name:        "ClearCache",
200 |         Description: "Clears cached workflow results, either by specific workflow or the entire cache.",
201 |         Parameters: JSONSchema{
202 |             Type: "object",
203 |             Properties: map[string]JSONSchemaProperty{
204 |                 "workflowName": {
205 |                     Type:        "string",
206 |                     Description: "Optional. Name of the workflow to clear the cache for. If not provided, all cache entries will be cleared.",
207 |                 },
208 |             },
209 |             Required: []string{},
210 |         },
211 |         Internal: true,
212 |     }
213 | 
214 |     return nil
215 | }
216 | ```
217 | 
218 | ### Enhanced Tool Invocation Router
219 | ```go
220 | func (s *MCPServer) handleToolCall(call ToolCallRequest) (ToolCallResponse, error) {
221 |     toolName := call.Name
222 |     tool, exists := s.tools[toolName]
223 |     if !exists {
224 |         return ToolCallResponse{}, fmt.Errorf("tool %s not found", toolName)
225 |     }
226 | 
227 |     // Handle special internal tools
228 |     if tool.Internal {
229 |         switch toolName {
230 |         case "ClearCache":
231 |             return s.handleClearCache(call.Parameters)
232 |         default:
233 |             return ToolCallResponse{}, fmt.Errorf("unknown internal tool: %s", toolName)
234 |         }
235 |     }
236 | 
237 |     // Regular workflow tool handling
238 |     // ...existing workflow execution code...
239 | }
240 | ```
241 | 
242 | ## 6. Configuration Example
243 | 
244 | ```yaml
245 | temporal:
246 |   # Connection configuration
247 |   hostPort: "localhost:7233"  # Local Temporal server
248 |   namespace: "default"
249 |   environment: "local"        # "local" or "remote"
250 | 
251 |   # Connection options
252 |   timeout: "5s"
253 |   retryOptions:
254 |     initialInterval: "100ms"
255 |     maximumInterval: "10s"
256 |     maximumAttempts: 5
257 |     backoffCoefficient: 2.0
258 | 
259 |   # For remote Temporal server (Temporal Cloud)
260 |   # environment: "remote"
261 |   # hostPort: "your-namespace.tmprl.cloud:7233"
262 |   # namespace: "your-namespace"
263 |   # auth:
264 |   #   clientID: "your-client-id"
265 |   #   clientSecret: "your-client-secret"
266 |   #   audience: "your-audience"
267 |   #   oauth2URL: "https://auth.temporal.io/oauth2/token"
268 |   # tls:
269 |   #   certPath: "/path/to/client.pem"
270 |   #   keyPath: "/path/to/client.key"
271 |   #   caPath: "/path/to/ca.pem"
272 |   #   serverName: "*.tmprl.cloud"
273 |   #   insecureSkipVerify: false
274 | 
275 | # Cache configuration
276 | cache:
277 |   enabled: true
278 |   databasePath: "./workflow_cache.db"
279 |   ttl: "24h"                # Cache entries expire after 24 hours
280 |   maxCacheSize: 104857600   # 100MB max cache size
281 |   cleanupInterval: "1h"     # Run cleanup every hour
282 | 
283 | workflows:
284 |   IngestWorkflow:
285 |     purpose: "Ingests documents into the vector store."
286 |     input:
287 |       type: "IngestRequest"
288 |       fields:
289 |         - doc_id: "The document ID to ingest."
290 |     output:
291 |       type: "string"
292 |       description: "ID of the ingested document."
293 |     taskQueue: "ingest-queue"
294 | 
295 |   UpdateXHRWorkflow:
296 |     purpose: "Updates XHR requests for DOM elements."
297 |     input:
298 |       type: "RAGRequest"
299 |       fields:
300 |         - session_id: "The session ID associated with the request."
301 |         - action: "The action to perform on the DOM element."
302 |     output:
303 |       type: "RAGResponse"
304 |       description: "The result of processing the fetched data."
305 |     taskQueue: "xhr-queue"
306 | 
307 |   ChatWorkflow:
308 |     purpose: "Processes chat completion requests."
309 |     input:
310 |       type: "ChatRequest"
311 |       fields:
312 |         - id: "The ID of the chat request."
313 |         - prompt_id: "The prompt ID for the chat."
314 |     output:
315 |       type: "string"
316 |       description: "The string completion response."
317 |     taskQueue: "chat-queue"
318 | 
319 |   NewChatMessageWorkflow:
320 |     purpose: "Processes new chat messages and updates JSON."
321 |     input:
322 |       type: "Message"
323 |       fields:
324 |         - client_id: "The client ID associated with the message."
325 |         - timestamp: "The timestamp of the message."
326 |         - content: "The content of the message."
327 |         - updated_json: "The updated JSON content."
328 |     output:
329 |       type: "Message"
330 |       description: "The message with explanation and updated JSON."
331 |     taskQueue: "message-queue"
332 | 
333 |   ProxieJSONWorkflow:
334 |     purpose: "Processes JSON payloads from Proxie."
335 |     input:
336 |       type: "ProxieJSONRequest"
337 |       fields:
338 |         - client_id: "The client ID associated with the request."
339 |         - content: "The content of the request."
340 |         - updated_json: "The updated JSON content."
341 |         - request_hash: "The request hash for tracking."
342 |     output:
343 |       type: "string"
344 |       description: "JSON string response for Proxie."
345 |     taskQueue: "json-queue"
346 | ```
347 | 
348 | ## 7. Security Considerations
349 | 
350 | ### Data Protection
351 | - No persistent storage of sensitive workflow data in MCP server
352 | - TLS for Temporal Cloud connections
353 | - Secure parameter handling
354 | 
355 | ### Validation
356 | - Input validation against schema before workflow execution
357 | - Configuration validation at startup
358 | - Response validation before returning to client
359 | 
360 | ## 8. Performance Requirements
361 | 
362 | ### Scalability
363 | - Support for multiple concurrent tool invocations
364 | - Efficient type conversion and serialization
365 | - Minimal memory footprint
366 | 
367 | ### Latency
368 | - Tool discovery response < 100ms
369 | - Tool invocation initialization < 200ms
370 | - Cache hits < 10ms
371 | 
372 | ## 9. Testing Strategy
373 | 
374 | ### Unit Testing
375 | - Configuration parsing
376 | - Tool definition generation
377 | - Cache operations
378 | - MCP message handling
379 | 
380 | ### Integration Testing
381 | - End-to-end workflow execution
382 | - Cache hit/miss scenarios
383 | - Cache clearing functionality
384 | 
385 | ## 10. Future Enhancements
386 | 
387 | ### Roadmap
388 | 1. **VERSION_0 (Initial Release)**:
389 |    - Basic YAML configuration parsing
390 |    - Dynamic tool definition generation
391 |    - Temporal workflow integration
392 |    - SQLite caching with clear functionality
393 |    - Stdio transport for MCP
394 | 
395 | 2. **VERSION_1**:
396 |    - Enhanced type system with automatic struct generation
397 |    - HTTP/SSE transport support
398 |    - Advanced cache analytics
399 |    - Improved error handling and reporting
400 | 
401 | 3. **VERSION_2**:
402 |    - Advanced workflow querying capabilities
403 |    - Metrics and monitoring integration
404 |    - Hot reloading of configuration
405 |    - Cloud-native deployment options
406 | 
407 | ## Conclusion
408 | 
409 | This specification provides a comprehensive blueprint for developing a Golang MCP server that dynamically exposes Temporal workflows as tools. The addition of the cache clear functionality provides important operational capabilities for managing the cache system, allowing for targeted clearing of specific workflow results or complete cache resets when necessary.
410 | 
411 | The design leverages Go's strong typing system and reflection capabilities while providing a clean, standardized interface for AI assistants to discover and invoke Temporal workflows through the MCP protocol.
```
--------------------------------------------------------------------------------
/cmd/temporal-mcp/main.go:
--------------------------------------------------------------------------------
```go
  1 | package main
  2 | 
  3 | import (
  4 | 	"context"
  5 | 	"flag"
  6 | 	"fmt"
  7 | 	"log"
  8 | 	"os"
  9 | 	"os/signal"
 10 | 	"strings"
 11 | 	"syscall"
 12 | 
 13 | 	"github.com/google/uuid"
 14 | 	"github.com/mocksi/temporal-mcp/internal/sanitize_history_event"
 15 | 	"google.golang.org/protobuf/encoding/protojson"
 16 | 
 17 | 	"text/template"
 18 | 
 19 | 	mcp "github.com/metoro-io/mcp-golang"
 20 | 	"github.com/metoro-io/mcp-golang/transport/stdio"
 21 | 	"github.com/mocksi/temporal-mcp/internal/config"
 22 | 	"github.com/mocksi/temporal-mcp/internal/temporal"
 23 | 	temporal_enums "go.temporal.io/api/enums/v1"
 24 | 	"go.temporal.io/sdk/client"
 25 | )
 26 | 
 27 | func main() {
 28 | 	// Parse command line arguments
 29 | 	configFile := flag.String("config", "config.yml", "Path to configuration file")
 30 | 	flag.Parse()
 31 | 
 32 | 	// CRITICAL: Configure all loggers to write to stderr instead of stdout
 33 | 	// This is essential as any output to stdout will corrupt the JSON-RPC stream
 34 | 	log.SetOutput(os.Stderr)
 35 | 	log.Println("Starting Temporal MCP server...")
 36 | 
 37 | 	// Setup signal handling for graceful shutdown
 38 | 	sigCh := make(chan os.Signal, 1)
 39 | 	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
 40 | 
 41 | 	// Load configuration
 42 | 	cfg, err := config.LoadConfig(*configFile)
 43 | 	if err != nil {
 44 | 		log.Fatalf("Failed to load configuration: %v", err)
 45 | 	}
 46 | 	log.Printf("Loaded configuration with %d workflows", len(cfg.Workflows))
 47 | 
 48 | 	// Initialize Temporal client
 49 | 	var temporalClient client.Client
 50 | 	var temporalError error
 51 | 
 52 | 	temporalClient, temporalError = temporal.NewTemporalClient(cfg.Temporal)
 53 | 	if temporalError != nil {
 54 | 		log.Printf("WARNING: Failed to connect to Temporal service: %v", temporalError)
 55 | 		log.Printf("MCP will run in degraded mode - workflow executions will return errors")
 56 | 	} else {
 57 | 		defer temporalClient.Close()
 58 | 		log.Printf("Connected to Temporal service at %s", cfg.Temporal.HostPort)
 59 | 	}
 60 | 
 61 | 	// Create a new MCP server with stdio transport for AI model communication
 62 | 	server := mcp.NewServer(stdio.NewStdioServerTransport())
 63 | 
 64 | 	// Create tool registry - used in future enhancements
 65 | 	// registry := tool.NewRegistry(cfg, temporalClient, cacheClient)
 66 | 
 67 | 	// Register all workflow tools
 68 | 	log.Println("Registering workflow tools...")
 69 | 	err = registerWorkflowTools(server, cfg, temporalClient)
 70 | 	if err != nil {
 71 | 		log.Fatalf("Failed to register workflow tools: %v", err)
 72 | 	}
 73 | 
 74 | 	// Register get workflow history tool
 75 | 	err = registerGetWorkflowHistoryTool(server, temporalClient)
 76 | 	if err != nil {
 77 | 		log.Fatalf("Failed to register get workflow history tool: %v", err)
 78 | 	}
 79 | 
 80 | 	// Register system prompt
 81 | 	err = registerSystemPrompt(server, cfg)
 82 | 	if err != nil {
 83 | 		log.Fatalf("Failed to register system prompt: %v", err)
 84 | 	}
 85 | 
 86 | 	// Start the MCP server in a goroutine
 87 | 	go func() {
 88 | 		log.Printf("Temporal MCP server is running. Press Ctrl+C to stop.")
 89 | 		if err := server.Serve(); err != nil {
 90 | 			log.Fatalf("Server error: %v", err)
 91 | 		}
 92 | 	}()
 93 | 
 94 | 	// Wait for termination signal
 95 | 	sig := <-sigCh
 96 | 	log.Printf("Received signal %v, shutting down MCP server...", sig)
 97 | 	log.Printf("Temporal MCP server has been stopped.")
 98 | }
 99 | 
100 | // registerWorkflowTools registers all workflow definitions as MCP tools
101 | func registerWorkflowTools(server *mcp.Server, cfg *config.Config, tempClient client.Client) error {
102 | 	// Register all workflows as tools
103 | 	for name, workflow := range cfg.Workflows {
104 | 		err := registerWorkflowTool(server, name, workflow, tempClient, cfg)
105 | 		if err != nil {
106 | 			return fmt.Errorf("failed to register workflow tool %s: %w", name, err)
107 | 		}
108 | 		log.Printf("Registered workflow tool: %s", name)
109 | 	}
110 | 
111 | 	return nil
112 | }
113 | 
114 | // registerWorkflowTool registers a single workflow as an MCP tool
115 | func registerWorkflowTool(server *mcp.Server, name string, workflow config.WorkflowDef, tempClient client.Client, cfg *config.Config) error {
116 | 	// Define the type for workflow parameters based on fields
117 | 	type WorkflowParams struct {
118 | 		Params     map[string]string `json:"params"`
119 | 		ForceRerun bool              `json:"force_rerun"`
120 | 	}
121 | 
122 | 	// Build detailed parameter descriptions for tool registration
123 | 	paramDescriptions := "\n\n**Parameters:**\n"
124 | 	for _, field := range workflow.Input.Fields {
125 | 		for fieldName, description := range field {
126 | 			isRequired := !strings.Contains(description, "Optional")
127 | 			if isRequired {
128 | 				paramDescriptions += fmt.Sprintf("- `%s` (required): %s\n", fieldName, description)
129 | 			} else {
130 | 				paramDescriptions += fmt.Sprintf("- `%s` (optional): %s\n", fieldName, description)
131 | 			}
132 | 		}
133 | 	}
134 | 
135 | 	// Add example usage
136 | 	paramDescriptions += "\n**Example Usage:**\n```json\n{\n  \"params\": {\n"
137 | 	paramExamples := []string{}
138 | 	for _, field := range workflow.Input.Fields {
139 | 		for fieldName, _ := range field {
140 | 			if strings.Contains(fieldName, "json") {
141 | 				paramExamples = append(paramExamples, fmt.Sprintf("    \"%s\": {\"example\": \"value\"}", fieldName))
142 | 			} else if strings.Contains(fieldName, "id") {
143 | 				paramExamples = append(paramExamples, fmt.Sprintf("    \"%s\": \"example-id-123\"", fieldName))
144 | 			} else {
145 | 				paramExamples = append(paramExamples, fmt.Sprintf("    \"%s\": \"example value\"", fieldName))
146 | 			}
147 | 		}
148 | 	}
149 | 	paramDescriptions += strings.Join(paramExamples, ",\n")
150 | 	paramDescriptions += "\n  },\n  \"force_rerun\": false\n}\n```"
151 | 
152 | 	// Create complete extended purpose description
153 | 	extendedPurpose := workflow.Purpose + paramDescriptions
154 | 
155 | 	// Register the tool with MCP server
156 | 	return server.RegisterTool(name, extendedPurpose, func(args WorkflowParams) (*mcp.ToolResponse, error) {
157 | 		// Check if Temporal client is available
158 | 		if tempClient == nil {
159 | 			log.Printf("Error: Temporal client is not available for workflow: %s", name)
160 | 			return mcp.NewToolResponse(mcp.NewTextContent(
161 | 				"Error: Temporal service is currently unavailable. Please try again later.",
162 | 			)), nil
163 | 		}
164 | 
165 | 		// Validate required parameters before execution
166 | 		if args.Params == nil {
167 | 			return mcp.NewToolResponse(mcp.NewTextContent(
168 | 				fmt.Sprintf("Error: No parameters provided for workflow %s. Please provide required parameters.", name),
169 | 			)), nil
170 | 		}
171 | 
172 | 		// Build list of required parameters
173 | 		var requiredParams []string
174 | 		for _, field := range workflow.Input.Fields {
175 | 			for fieldName, description := range field {
176 | 				if !strings.Contains(description, "Optional") {
177 | 					requiredParams = append(requiredParams, fieldName)
178 | 				}
179 | 			}
180 | 		}
181 | 
182 | 		// Check for missing required parameters
183 | 		var missingParams []string
184 | 		for _, param := range requiredParams {
185 | 			if _, exists := args.Params[param]; !exists || args.Params[param] == "" {
186 | 				missingParams = append(missingParams, param)
187 | 			}
188 | 		}
189 | 
190 | 		// Return error if any required parameters are missing
191 | 		if len(missingParams) > 0 {
192 | 			missingParamsList := strings.Join(missingParams, ", ")
193 | 			return mcp.NewToolResponse(mcp.NewTextContent(
194 | 				fmt.Sprintf("Error: Missing required parameters for workflow %s: %s", name, missingParamsList),
195 | 			)), nil
196 | 		}
197 | 
198 | 		// Execute the workflow
199 | 		// Determine which task queue to use (workflow-specific or default)
200 | 		taskQueue := workflow.TaskQueue
201 | 		if taskQueue == "" && cfg != nil {
202 | 			taskQueue = cfg.Temporal.DefaultTaskQueue
203 | 			log.Printf("Using default task queue: %s for workflow %s", taskQueue, name)
204 | 		}
205 | 
206 | 		workflowID, err := computeWorkflowID(workflow, args.Params)
207 | 		if err != nil {
208 | 			log.Printf("Error computing workflow ID from arguments: %v", err)
209 | 			return mcp.NewToolResponse(mcp.NewTextContent(
210 | 				fmt.Sprintf("Error computing workflow ID from arguments: %v", err),
211 | 			)), nil
212 | 		}
213 | 
214 | 		if workflowID == "" {
215 | 			log.Printf("Workflow %q has an empty or missing workflowIDRecipe - using a random workflow id", name)
216 | 			workflowID = uuid.NewString()
217 | 		}
218 | 
219 | 		// This will execute a new workflow when:
220 | 		// - there is no workflow with the given id
221 | 		// - there is a failed workflow with the given id (e.g. terminated, failed, timed out)
222 | 		// and attach to an existing workflow when:
223 | 		// - there is a running workflow with the given id
224 | 		// - there is a successful workflow with the given id
225 | 		//
226 | 		// Note that temporal's data retention window (a setting on each namespace) influences the behavior above
227 | 		reusePolicy := temporal_enums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY
228 | 		conflictPolicy := temporal_enums.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING
229 | 
230 | 		if args.ForceRerun {
231 | 			// This will execute a new workflow in all cases. If there is a running workflow with the given id, it will
232 | 			// be terminated.
233 | 			reusePolicy = temporal_enums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE
234 | 			conflictPolicy = temporal_enums.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING
235 | 		}
236 | 
237 | 		wfOptions := client.StartWorkflowOptions{
238 | 			TaskQueue:                taskQueue,
239 | 			ID:                       workflowID,
240 | 			WorkflowIDReusePolicy:    reusePolicy,
241 | 			WorkflowIDConflictPolicy: conflictPolicy,
242 | 		}
243 | 
244 | 		log.Printf("Starting workflow %s on task queue %s", name, taskQueue)
245 | 
246 | 		// Start workflow execution
247 | 		run, err := tempClient.ExecuteWorkflow(context.Background(), wfOptions, name, args.Params)
248 | 		if err != nil {
249 | 			log.Printf("Error starting workflow %s: %v", name, err)
250 | 			return mcp.NewToolResponse(mcp.NewTextContent(
251 | 				fmt.Sprintf("Error executing workflow: %v", err),
252 | 			)), nil
253 | 		}
254 | 
255 | 		log.Printf("Workflow started: WorkflowID=%s RunID=%s", run.GetID(), run.GetRunID())
256 | 
257 | 		// Wait for workflow completion
258 | 		var result string
259 | 		if err := run.Get(context.Background(), &result); err != nil {
260 | 			log.Printf("Error in workflow %s execution: %v", name, err)
261 | 			return mcp.NewToolResponse(mcp.NewTextContent(
262 | 				fmt.Sprintf("Workflow failed: %v", err),
263 | 			)), nil
264 | 		}
265 | 
266 | 		log.Printf("Workflow %s completed successfully", name)
267 | 
268 | 		return mcp.NewToolResponse(mcp.NewTextContent(result)), nil
269 | 	})
270 | }
271 | 
272 | func computeWorkflowID(workflow config.WorkflowDef, params map[string]string) (string, error) {
273 | 	tmpl := template.New("id_recipe")
274 | 
275 | 	tmpl.Funcs(template.FuncMap{
276 | 		"hash": func(paramsToHash ...any) (string, error) {
277 | 			return hashWorkflowArgs(params, paramsToHash...)
278 | 		},
279 | 	})
280 | 	if _, err := tmpl.Parse(workflow.WorkflowIDRecipe); err != nil {
281 | 		return "", err
282 | 	}
283 | 
284 | 	writer := strings.Builder{}
285 | 	if err := tmpl.Execute(&writer, params); err != nil {
286 | 		return "", err
287 | 	}
288 | 
289 | 	return writer.String(), nil
290 | }
291 | 
292 | // registerGetWorkflowHistoryTool registres a tool that gets workflow histories
293 | func registerGetWorkflowHistoryTool(server *mcp.Server, tempClient client.Client) error {
294 | 	type GetWorkflowHistoryParams struct {
295 | 		WorkflowID string `json:"workflowId"`
296 | 		RunID      string `json:"runId"`
297 | 	}
298 | 	desc := "Gets the workflow execution history for a specific run of a workflow. runId is optional - if omitted, this tool gets the history for the latest run of the given workflowId"
299 | 
300 | 	return server.RegisterTool("GetWorkflowHistory", desc, func(args GetWorkflowHistoryParams) (*mcp.ToolResponse, error) {
301 | 		// Check if Temporal client is available
302 | 		if tempClient == nil {
303 | 			log.Printf("Error: Temporal client is not available for getting workflow histories")
304 | 			return mcp.NewToolResponse(mcp.NewTextContent(
305 | 				"Error: Temporal client is not available for getting workflow histories",
306 | 			)), nil
307 | 		}
308 | 
309 | 		eventJsons := make([]string, 0)
310 | 		iterator := tempClient.GetWorkflowHistory(context.Background(), args.WorkflowID, args.RunID, false, temporal_enums.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT)
311 | 		for iterator.HasNext() {
312 | 			event, err := iterator.Next()
313 | 			if err != nil {
314 | 				msg := fmt.Sprintf("Error: Failed to get %dth history event: %v", len(eventJsons), err)
315 | 				log.Print(msg)
316 | 				return mcp.NewToolResponse(mcp.NewTextContent(msg)), nil
317 | 			}
318 | 
319 | 			sanitize_history_event.SanitizeHistoryEvent(event)
320 | 			bytes, err := protojson.Marshal(event)
321 | 			if err != nil {
322 | 				// should never happen?
323 | 				return nil, err
324 | 			}
325 | 
326 | 			eventJsons = append(eventJsons, string(bytes))
327 | 		}
328 | 
329 | 		// The last step of json-marshalling is unfortunate (forced on us by the lack of a proto for the list of
330 | 		// events), but not worth actually building and marshalling a slice for. Let's just do it by hand.
331 | 		allEvents := strings.Builder{}
332 | 		allEvents.WriteString("[")
333 | 		for i, eventJson := range eventJsons {
334 | 			if i > 0 {
335 | 				allEvents.WriteString(",")
336 | 			}
337 | 			allEvents.WriteString(eventJson)
338 | 		}
339 | 		allEvents.WriteString("]")
340 | 
341 | 		return mcp.NewToolResponse(mcp.NewTextContent(allEvents.String())), nil
342 | 	})
343 | }
344 | 
345 | // registerSystemPrompt registers the system prompt for the MCP
346 | func registerSystemPrompt(server *mcp.Server, cfg *config.Config) error {
347 | 	return server.RegisterPrompt("system_prompt", "System prompt for the Temporal MCP", func(_ struct{}) (*mcp.PromptResponse, error) {
348 | 		// Build list of available tools from workflows
349 | 		workflowList := ""
350 | 		for name, workflow := range cfg.Workflows {
351 | 			// Use the complete purpose which already includes parameter details from config.yml
352 | 			detailedPurpose := workflow.Purpose
353 | 
354 | 			workflowList += fmt.Sprintf("## %s\n", name)
355 | 			workflowList += fmt.Sprintf("**Purpose:** %s\n\n", detailedPurpose)
356 | 			workflowList += fmt.Sprintf("**Input Type:** %s\n\n", workflow.Input.Type)
357 | 
358 | 			// Add parameters section with detailed formatting based on the Input.Fields
359 | 			workflowList += "**Parameters:**\n"
360 | 			for _, field := range workflow.Input.Fields {
361 | 				for fieldName, description := range field {
362 | 					isRequired := !strings.Contains(description, "Optional")
363 | 					if isRequired {
364 | 						workflowList += fmt.Sprintf("- `%s` (required): %s\n", fieldName, description)
365 | 					} else {
366 | 						workflowList += fmt.Sprintf("- `%s` (optional): %s\n", fieldName, description)
367 | 					}
368 | 				}
369 | 			}
370 | 
371 | 			// Add example of how to call this workflow
372 | 			workflowList += "\n**Example Usage:**\n"
373 | 			workflowList += "```json\n"
374 | 			workflowList += "{\n  \"params\": {\n"
375 | 
376 | 			// Generate example parameters
377 | 			paramExamples := []string{}
378 | 			for _, field := range workflow.Input.Fields {
379 | 				for fieldName, _ := range field {
380 | 					if strings.Contains(fieldName, "json") {
381 | 						paramExamples = append(paramExamples, fmt.Sprintf("    \"%s\": {\"example\": \"value\"}", fieldName))
382 | 					} else if strings.Contains(fieldName, "id") {
383 | 						paramExamples = append(paramExamples, fmt.Sprintf("    \"%s\": \"example-id-123\"", fieldName))
384 | 					} else {
385 | 						paramExamples = append(paramExamples, fmt.Sprintf("    \"%s\": \"example value\"", fieldName))
386 | 					}
387 | 				}
388 | 			}
389 | 			workflowList += strings.Join(paramExamples, ",\n")
390 | 			workflowList += "\n  },\n  \"force_rerun\": false\n}\n```\n"
391 | 
392 | 			// Add output information
393 | 			workflowList += fmt.Sprintf("\n**Output Type:** %s\n", workflow.Output.Type)
394 | 			if workflow.Output.Description != "" {
395 | 				workflowList += fmt.Sprintf("**Output Description:** %s\n", workflow.Output.Description)
396 | 			}
397 | 
398 | 			// Extract required parameters for validation guidance
399 | 			var requiredParams []string
400 | 			for _, field := range workflow.Input.Fields {
401 | 				for fieldName, description := range field {
402 | 					if !strings.Contains(description, "Optional") {
403 | 						requiredParams = append(requiredParams, fieldName)
404 | 					}
405 | 				}
406 | 			}
407 | 
408 | 			// Add validation guidelines
409 | 			if len(requiredParams) > 0 {
410 | 				workflowList += "\n**Required Validation:**\n"
411 | 				workflowList += "- Validate all required parameters are provided before execution\n"
412 | 				paramsList := strings.Join(requiredParams, ", ")
413 | 				workflowList += fmt.Sprintf("- Required parameters: %s\n", paramsList)
414 | 			}
415 | 
416 | 			workflowList += "\n---\n\n"
417 | 		}
418 | 
419 | 		systemPrompt := fmt.Sprintf(`You are now connected to a Temporal MCP (Model Control Protocol) server that provides access to various Temporal workflows.
420 | 
421 | This MCP exposes the following workflow tools:
422 | 
423 | %s
424 | ## Parameter Validation Guidelines
425 | 
426 | Before executing any workflow, ensure you:
427 | 
428 | 1. Validate all required parameters are present and properly formatted
429 | 2. Check that string parameters have appropriate length and format
430 | 3. Verify numeric parameters are within expected ranges
431 | 4. Ensure any IDs follow the proper format guidelines
432 | 5. Ask the user for any missing required parameters before execution
433 | 
434 | ## Tool Usage Instructions
435 | 
436 | Use these tools to help users interact with Temporal workflows. Each workflow requires a 'params' object containing the necessary parameters listed above.
437 | 
438 | When constructing your calls:
439 | - Include all required parameters
440 | - Set force_rerun to true only when explicitly requested by the user
441 | - When force_rerun is false, Temporal will deduplicate workflows based on their arguments
442 | 
443 | ## General Example Structure
444 | 
445 | To call any workflow:
446 | `+"```"+`
447 | {
448 |   "params": {
449 |     "param1": "value1",
450 |     "param2": "value2"
451 |   },
452 |   "force_rerun": false
453 | }
454 | `+"```"+`
455 | 
456 | Refer to each workflow's specific example above for exact parameter requirements.`, workflowList)
457 | 
458 | 		return mcp.NewPromptResponse("system_prompt", mcp.NewPromptMessage(mcp.NewTextContent(systemPrompt), mcp.Role("system"))), nil
459 | 	})
460 | }
461 | 
```