# 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:
--------------------------------------------------------------------------------
```
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
vendor/
# Go workspace file
go.work
# IDE specific files
.idea/
.vscode/
*.swp
*.swo
# OS specific files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Build output
bin/
dist/
build/
# Environment variables
.env
.env.local
# Configuration files
config.yml
examples/claude_config.json
# Database files
*.db
```
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
```markdown
# Temporal MCP Examples
This directory contains examples and configuration for the Temporal Model Context Protocol (MCP) server that exposes Temporal workflows as tools for AI assistants.
## What is MCP?
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.
## What is Temporal?
[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.
## Features
The Temporal MCP server provides access to workflows configured in `config.yml`, such as:
1. **Dynamic workflow execution** - Run any workflow defined in the configuration
2. **Cached results** - Optionally cache workflow results for improved performance
3. **Task queue management** - Configure specific or default task queues for workflow execution
## Using with Claude Desktop
### Automatic Configuration (Recommended)
The easiest way to configure Claude Desktop is to use the provided script:
1. Build the MCP server using the Makefile from the root directory:
```bash
cd .. && make build
```
2. Run the configuration script from the examples directory:
```bash
./generate_claude_config.sh
```
This will:
- Generate a `claude_config.json` file with correct paths for your system
- Add the file to .gitignore to prevent committing personal paths
- Show instructions for deploying the config file
3. Copy the generated config to Claude's configuration directory:
```bash
cp claude_config.json ~/Library/Application\ Support/Claude/claude_desktop_config.json
```
4. Restart Claude Desktop
### Manual Configuration
Alternatively, you can manually create a configuration file at `~/Library/Application Support/Claude/claude_desktop_config.json` with the following content:
```json
{
"mcpServers": {
"temporal-mcp": {
"command": "/full/path/to/your/bin/temporal-mcp",
"args": ["--config", "/full/path/to/your/config.yml"],
"env": {}
}
}
}
```
Remember to replace the paths with the actual full paths to your binaries and config file.
4. When chatting with Claude, you can ask it to use the Pig Latin conversion tools.
## Example Prompts for Claude
### Temporal MCP
Once connected to the Temporal MCP server, you can ask Claude things like:
- "Can you run the GreetingWorkflow with my name as a parameter?"
- "Please execute the DataProcessingWorkflow with the following parameters..."
- "Clear the cache for all workflows"
- "Run the AnalyticsWorkflow and show me the results"
## How It Works
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:
1. It recognizes the need to execute a Temporal workflow
2. It calls the appropriate workflow tool with the required parameters
3. The MCP server executes the workflow on the Temporal service
4. The workflow result is returned to Claude
5. Claude presents the result to the user
The Temporal MCP server also supports result caching to improve performance for repetitive workflow executions.
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# ⏰🧠 Temporal-MCP Server
[](https://deepwiki.com/Mocksi/temporal-mcp)
[](https://github.com/Mocksi/temporal-mcp/actions/workflows/go-test.yml)
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.
## Why Temporal MCP
- **Supercharged AI** — AI assistants gain reliable, long-running workflow superpowers
- **Conversational Orchestration** — Trigger, monitor, and manage workflows through natural language
- **Enterprise-Ready** — Leverage Temporal's retries, timeouts, and persistence—exposed in plain text
## ✨ Key Features
- **🔍 Automatic Discovery** — Explore available workflows and see rich metadata
- **🏃♂️ Seamless Execution** — Kick off complex processes with a single chat message
- **📊 Real-time Monitoring** — Follow progress, check status, and get live updates
- **⚡ Performance Optimization** — Smart caching for instant answers
- **🧠 AI-Friendly Descriptions** — Purpose fields written for both humans and machines
## 🏁 Getting Started
### Prerequisites
- **Go 1.21+** — For building and running the MCP server
- **Temporal Server** — Running locally or remotely (see [Temporal docs](https://docs.temporal.io/docs/server/quick-install/))
### Quick Install
1. Run your Temporal server and workers
In this example, we'll use the [Temporal Money Transfer Demo](https://github.com/temporal-sa/money-transfer-demo/tree/main).
#### MCP Setup
Get Claude (or similar MCP-enabled AI assistant) talking to your workflows in 5 easy steps:
2. **Build the server**
```bash
git clone https://github.com/Mocksi/temporal-mcp.git
cd temporal-mcp
make build
```
2. **Define your workflows** in `config.yml`
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):
```yaml
workflows:
AccountTransferWorkflow:
purpose: "Transfers money between accounts with validation and notification. Handles the happy path scenario where everything works as expected."
input:
type: "TransferInput"
fields:
- from_account: "Source account ID"
- to_account: "Destination account ID"
- amount: "Amount to transfer"
output:
type: "TransferOutput"
description: "Transfer confirmation with charge ID"
taskQueue: "account-transfer-queue"
AccountTransferWorkflowScenarios:
purpose: "Extended account transfer workflow with various scenarios including human approval, recoverable failures, and advanced visibility features."
input:
type: "TransferInput"
fields:
- from_account: "Source account ID"
- to_account: "Destination account ID"
- amount: "Amount to transfer"
- scenario_type: "Type of scenario to execute (human_approval, recoverable_failure, advanced_visibility)"
output:
type: "TransferOutput"
description: "Transfer confirmation with charge ID"
taskQueue: "account-transfer-queue"
```
3. **Generate Claude's configuration**
```bash
cd examples
./generate_claude_config.sh
```
4. **Install the configuration**
```bash
cp examples/claude_config.json ~/Library/Application\ Support/Claude/claude_desktop_config.json
```
5. **Start Claude** with this configuration
### Conversing with Your Workflows
Now for the magic part! Talk to your workflows through Claude using natural language:
> 💬 "Claude, can you transfer $100 from account ABC123 to account XYZ789?"
> 💬 "What transfer scenarios are available to test?"
> 💬 "Execute a transfer that requires human approval for $500 between accounts ABC123 and XYZ789"
> 💬 "Has the transfer workflow completed yet?"
> 💬 "Run a transfer scenario with recoverable failures to test error handling"
Behind the scenes, Temporal MCP translates these natural language requests into properly formatted workflow executions—no more complex API calls or parameter formatting!
## Core Values
1. **Clarity First** — Use simple, direct language. Avoid jargon.
2. **Benefit-Driven** — Lead with "what's in it for me".
3. **Concise Power** — Less is more—keep sentences tight and memorable.
4. **Personality & Voice** — Bold statements, short lines, a dash of excitement.
## Ready to Showcase
Lights, camera, action—capture your first AI-driven workflow in motion. Share that moment. Inspire others to see Temporal MCP in action.
## Development
### Project Structure
```
./
├── cmd/ # Entry points for executables
├── internal/ # Internal package code
│ ├── api/ # MCP API implementation
│ ├── cache/ # Caching layer
│ ├── config/ # Configuration management
│ └── temporal/ # Temporal client integration
├── examples/ # Example configurations and scripts
└── docs/ # Documentation
```
### Common Commands
| Command | Description |
|---------|-------------|
| `make build` | Builds the binary in `./bin` |
| `make test` | Runs all unit tests |
| `make fmt` | Formats code according to Go standards |
| `make run` | Builds and runs the server |
| `make clean` | Removes build artifacts |
## 🔍 Troubleshooting
### Common Issues
**Connection Refused**
- ✓ Check if Temporal server is running
- ✓ Verify hostPort is correct in config.yml
**Workflow Not Found**
- ✓ Ensure workflow is registered in Temporal
- ✓ Check namespace settings in config.yml
**Claude Can't See Workflows**
- ✓ Verify claude_config.json is in the correct location
- ✓ Restart Claude after configuration changes
## ⚙️ Configuration
The heart of Temporal MCP is its configuration file, which connects your AI assistants to your workflow engine:
### Configuration Architecture
Your `config.yml` consists of three key sections:
1. **🔌 Temporal Connection** — How to connect to your Temporal server
2. **💾 Cache Settings** — Performance optimization for workflow results
3. **🔧 Workflow Definitions** — The workflows your AI can discover and use
### Example Configuration
The sample configuration is designed to work with the Temporal Money Transfer Demo:
```yaml
# Temporal server connection details
temporal:
hostPort: "localhost:7233" # Your Temporal server address
namespace: "default" # Temporal namespace
environment: "local" # "local" or "remote"
defaultTaskQueue: "account-transfer-queue" # Default task queue for workflows
# Fine-tune connection behavior
timeout: "5s" # Connection timeout
retryOptions: # Robust retry settings
initialInterval: "100ms" # Start with quick retries
maximumInterval: "10s" # Max wait between retries
maximumAttempts: 5 # Don't try forever
backoffCoefficient: 2.0 # Exponential backoff
# Define AI-discoverable workflows
workflows:
AccountTransferWorkflow:
purpose: "Transfers money between accounts with validation and notification. Handles the happy path scenario where everything works as expected."
workflowIDRecipe: "transfer_{{.from_account}}_{{.to_account}}_{{.amount}}"
input:
type: "TransferInput"
fields:
- from_account: "Source account ID"
- to_account: "Destination account ID"
- amount: "Amount to transfer"
output:
type: "TransferOutput"
description: "Transfer confirmation with charge ID"
taskQueue: "account-transfer-queue"
activities:
- name: "validate"
timeout: "5s"
- name: "withdraw"
timeout: "5s"
- name: "deposit"
timeout: "5s"
- name: "sendNotification"
timeout: "5s"
- name: "undoWithdraw"
timeout: "5s"
```
> 💡 **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.
## 💎 Best Practices
### Crafting Perfect Purpose Fields
The `purpose` field is your AI assistant's window into understanding what each workflow does. Make it count!
#### ✅ Do This
- Write clear, detailed descriptions of functionality
- Mention key parameters and how they customize behavior
- Describe expected outputs and their formats
- Note any limitations or constraints
#### ❌ Avoid This
- Vague descriptions ("Processes data")
- Technical jargon without explanation
- Missing important parameters
- Ignoring error cases or limitations
#### Before & After
**Before:** "Gets information about a file."
**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."
### Naming Conventions
| Item | Convention | Example |
|------|------------|----------|
| Workflow IDs | PascalCase | `AccountTransferWorkflow` |
| Parameter names | snake_case | `from_account`, `to_account` |
| Parameters with units | Include unit | `timeout_seconds`, `amount` |
### Security Guidelines
⚠️ **Important Security Notes:**
- Keep credentials out of your configuration files
- Use environment variables for sensitive values
- Consider access controls for workflows with sensitive data
- Validate and sanitize all workflow inputs
> 💡 **Tip:** Create different configurations for development and production environments
### Why Good Purpose Fields Matter
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
2. **Fewer Errors** - Detailed descriptions reduce the chances of AI systems using components incorrectly
3. **Improved Debugging** - Clear descriptions help identify issues when workflows don't behave as expected
4. **Better Developer Experience** - New team members can understand your system more quickly
5. **Documentation As Code** - Purpose fields serve as living documentation that stays in sync with the codebase
## Contribute & Collaborate
We're building this together.
- Share your own workflow configs
- Improve descriptions
- Share your demos (video or GIF) in issues
Let's unleash the power of AI and Temporal together!
## 📜 License
This project is licensed under the MIT License - see the LICENSE file for details.
Contributions welcome!
```
--------------------------------------------------------------------------------
/internal/tool/definition.go:
--------------------------------------------------------------------------------
```go
package tool
// Definition represents an MCP tool definition
type Definition struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters Schema `json:"parameters"`
Internal bool `json:"-"` // Flag for internal tools like ClearCache
}
// Schema represents a JSON Schema for tool parameters
type Schema struct {
Type string `json:"type"`
Properties map[string]SchemaProperty `json:"properties"`
Required []string `json:"required"`
}
// SchemaProperty represents a property in a JSON Schema
type SchemaProperty struct {
Type string `json:"type"`
Description string `json:"description"`
}
```
--------------------------------------------------------------------------------
/cmd/main.go:
--------------------------------------------------------------------------------
```go
package main
import (
"log"
"os"
"os/signal"
"syscall"
)
func main() {
// Configure logger to write to stderr
log.SetOutput(os.Stderr)
log.Println("Starting Temporal MCP...")
// Setup signal handling for graceful shutdown
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
// TODO: Initialize configuration
// TODO: Setup Temporal client
// TODO: Initialize services
// TODO: Start API server
log.Println("Temporal MCP is running. Press Ctrl+C to stop.")
// Wait for termination signal
sig := <-sigCh
log.Printf("Received signal %v, shutting down...", sig)
// TODO: Perform cleanup and graceful shutdown
log.Println("Temporal MCP has been stopped.")
}
```
--------------------------------------------------------------------------------
/internal/temporal/logger.go:
--------------------------------------------------------------------------------
```go
package temporal
import (
"log"
)
// StderrLogger implements the Temporal logger interface
// ensuring all Temporal logs go to stderr instead of stdout
type StderrLogger struct {
logger *log.Logger
}
// Debug logs a debug message
func (l *StderrLogger) Debug(msg string, keyvals ...interface{}) {
l.logger.Printf("[DEBUG] %s", msg)
}
// Info logs an info message
func (l *StderrLogger) Info(msg string, keyvals ...interface{}) {
l.logger.Printf("[INFO] %s", msg)
}
// Warn logs a warning message
func (l *StderrLogger) Warn(msg string, keyvals ...interface{}) {
l.logger.Printf("[WARN] %s", msg)
}
// Error logs an error message
func (l *StderrLogger) Error(msg string, keyvals ...interface{}) {
l.logger.Printf("[ERROR] %s", msg)
}
```
--------------------------------------------------------------------------------
/.github/workflows/go-test.yml:
--------------------------------------------------------------------------------
```yaml
name: Go Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
cache: true
- name: Install dependencies
run: go mod download
- name: Verify dependencies
run: go mod verify
- name: Run tests
run: go test -v ./...
- name: Run formatting check
run: |
if [ -n "$(gofmt -l .)" ]; then
echo "The following files need to be formatted:"
gofmt -l .
exit 1
fi
- name: Run vet
run: go vet ./...
```
--------------------------------------------------------------------------------
/cmd/temporal-mcp/hash_args.go:
--------------------------------------------------------------------------------
```go
package main
import (
"encoding/json"
"fmt"
"hash/fnv"
"log"
)
// hashWorkflowArgs produces a short (suitable for inclusion in workflow id) hash of the given arguments. Args must be
// json.Marshal-able.
func hashWorkflowArgs(allParams map[string]string, paramsToHash ...any) (string, error) {
if len(paramsToHash) == 0 {
log.Printf("Warning: No hash arguments provided - will hash all arguments. Please replace {{ hash }} with {{ hash . }} in the workflowIDRecipe")
paramsToHash = []any{allParams}
}
hasher := fnv.New32()
for _, arg := range paramsToHash {
// important: json.Marshal sorts map keys
bytes, err := json.Marshal(arg)
if err != nil {
return "", err
}
_, _ = hasher.Write(bytes)
}
return fmt.Sprintf("%d", hasher.Sum32()), nil
}
```
--------------------------------------------------------------------------------
/internal/tool/registry.go:
--------------------------------------------------------------------------------
```go
// Package tool provides utilities for working with Temporal workflows as MCP tools
package tool
import (
"github.com/mocksi/temporal-mcp/internal/config"
"go.temporal.io/sdk/client"
)
// Registry manages workflow tools metadata and dependencies
type Registry struct {
config *config.Config
tempClient client.Client
}
// NewRegistry creates a new tool registry with required dependencies
func NewRegistry(cfg *config.Config, tempClient client.Client) *Registry {
return &Registry{
config: cfg,
tempClient: tempClient,
}
}
// GetConfig returns the configuration used by this registry
func (r *Registry) GetConfig() *config.Config {
return r.config
}
// GetTemporalClient returns the Temporal client instance
func (r *Registry) GetTemporalClient() client.Client {
return r.tempClient
}
```
--------------------------------------------------------------------------------
/internal/tool/registry_test.go:
--------------------------------------------------------------------------------
```go
package tool
import (
"testing"
"github.com/mocksi/temporal-mcp/internal/config"
)
// TestRegistryGetters tests the Registry getters
func TestRegistryGetters(t *testing.T) {
// Create test objects
cfg := &config.Config{}
// Create registry directly without using interfaces to avoid lint errors
registry := &Registry{
config: cfg,
}
// Test GetConfig
if registry.GetConfig() != cfg {
t.Error("GetConfig did not return the expected config")
}
// For GetTemporalClient, we can only check it's not nil
// since we can't directly compare interface values
// Skip testing GetTemporalClient to avoid interface implementation issues
}
// TestNewRegistry tests the NewRegistry constructor
func TestNewRegistry(t *testing.T) {
// Create test objects
cfg := &config.Config{}
// Since we can't easily mock the client.Client interface in tests,
// we'll create the registry directly instead of using NewRegistry
// Create a registry directly
registry := &Registry{
config: cfg,
}
// Test just the config and cacheClient properties
if registry.config != cfg {
t.Error("Registry not initialized with the correct config")
}
}
```
--------------------------------------------------------------------------------
/internal/config/config.go:
--------------------------------------------------------------------------------
```go
package config
import (
"gopkg.in/yaml.v3"
"os"
)
// Config holds the top-level configuration
type Config struct {
Temporal TemporalConfig `yaml:"temporal"`
Workflows map[string]WorkflowDef `yaml:"workflows"`
}
// TemporalConfig defines connection settings for Temporal service
type TemporalConfig struct {
HostPort string `yaml:"hostPort"`
Namespace string `yaml:"namespace"`
Environment string `yaml:"environment"`
Timeout string `yaml:"timeout,omitempty"`
DefaultTaskQueue string `yaml:"defaultTaskQueue,omitempty"`
}
// WorkflowDef describes a Temporal workflow exposed as a tool
type WorkflowDef struct {
Purpose string `yaml:"purpose"`
Input ParameterDef `yaml:"input"`
Output ParameterDef `yaml:"output"`
TaskQueue string `yaml:"taskQueue"`
WorkflowIDRecipe string `yaml:"workflowIDRecipe"`
}
// ParameterDef defines input/output schema for a workflow
type ParameterDef struct {
Type string `yaml:"type"`
Fields []map[string]string `yaml:"fields"`
Description string `yaml:"description,omitempty"`
}
// LoadConfig reads and parses YAML config from file
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var cfg Config
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
}
return &cfg, nil
}
```
--------------------------------------------------------------------------------
/internal/temporal/client.go:
--------------------------------------------------------------------------------
```go
package temporal
import (
"fmt"
"log"
"os"
"time"
"github.com/mocksi/temporal-mcp/internal/config"
"go.temporal.io/sdk/client"
)
// NewTemporalClient creates a Temporal client based on the provided configuration
func NewTemporalClient(cfg config.TemporalConfig) (client.Client, error) {
// Validate timeout format if specified
if cfg.Timeout != "" {
_, err := time.ParseDuration(cfg.Timeout)
if err != nil {
return nil, fmt.Errorf("invalid timeout format: %w", err)
}
// Note: We're only validating the format, actual timeout handling would be implemented here
}
// Configure a logger that uses stderr
tempLogger := log.New(os.Stderr, "[temporal] ", log.LstdFlags)
// Create Temporal logger adapter that ensures all logs go to stderr
temporalLogger := &StderrLogger{logger: tempLogger}
// Set client options
options := client.Options{
HostPort: cfg.HostPort,
Namespace: cfg.Namespace,
Logger: temporalLogger,
}
// Handle environment-specific configuration
switch cfg.Environment {
case "local":
// Local Temporal server (default settings)
case "remote":
// To be implemented for remote/cloud Temporal connections
// This would include TLS and authentication setup
return nil, fmt.Errorf("remote environment configuration not implemented yet")
default:
return nil, fmt.Errorf("unsupported environment type: %s", cfg.Environment)
}
// Create the client
temporalClient, err := client.Dial(options)
if err != nil {
return nil, fmt.Errorf("failed to create Temporal client: %w", err)
}
return temporalClient, nil
}
```
--------------------------------------------------------------------------------
/examples/generate_claude_config.sh:
--------------------------------------------------------------------------------
```bash
#!/bin/bash
# Script to generate a claude_config.json file with correct paths
# This should be run from the examples directory
set -e # Exit on error
# Get the parent directory of the examples folder
PARENT_DIR="$(cd .. && pwd)"
# Define the output file
CONFIG_FILE="claude_config.json"
# Check if we're in the examples directory
if [[ "$(basename $(pwd))" != "examples" ]]; then
echo "Error: This script must be run from the examples directory"
exit 1
fi
# Check if binary exists
if [[ ! -f "$PARENT_DIR/bin/temporal-mcp" ]]; then
echo "Warning: temporal-mcp binary not found. Make sure to build it first with 'make build'"
fi
# Generate the JSON configuration file
cat > "$CONFIG_FILE" << EOF
{
"mcpServers": {
"temporal-mcp": {
"command": "$PARENT_DIR/bin/temporal-mcp",
"args": ["--config", "$PARENT_DIR/config.yml"],
"env": {}
}
}
}
EOF
echo "Generated $CONFIG_FILE with correct paths"
# Add file to .gitignore if it's not already there
GITIGNORE_FILE="$PARENT_DIR/.gitignore"
if [[ -f "$GITIGNORE_FILE" ]]; then
if ! grep -q "examples/$CONFIG_FILE" "$GITIGNORE_FILE"; then
echo "Adding $CONFIG_FILE to .gitignore"
echo "examples/$CONFIG_FILE" >> "$GITIGNORE_FILE"
else
echo "$CONFIG_FILE is already in .gitignore"
fi
else
echo "Creating .gitignore and adding $CONFIG_FILE"
echo "examples/$CONFIG_FILE" > "$GITIGNORE_FILE"
fi
# Instructions for the user
echo ""
echo "To use this configuration with Claude:"
echo "1. Copy this file to Claude's configuration directory:"
echo " cp $CONFIG_FILE ~/Library/Application\\ Support/Claude/claude_desktop_config.json"
echo "2. Restart Claude if it's already running"
echo ""
echo "Alternatively, you can reference this file in your Claude installation settings."
echo "See the README.md for more information."
# Make the script executable
chmod +x "$0"
```
--------------------------------------------------------------------------------
/internal/sanitize_history_event/sanitize_history_event.go:
--------------------------------------------------------------------------------
```go
package sanitize_history_event
import (
"go.temporal.io/api/history/v1"
"google.golang.org/protobuf/reflect/protoreflect"
"strings"
)
// SanitizeHistoryEvent removes all Payloads from the given history event's attributes. This helps mitigate the impact of
// large workflow histories (temporal permits up to 50mb) on small LLM context windows (~2mb). This is just best
// effort - it assumes that largeness is caused by the payloads.
func SanitizeHistoryEvent(event *history.HistoryEvent) {
sanitizeRecursively(event.ProtoReflect())
}
// HistoryEvents are highly polymorphic (today: 54 different types), and Temporal could add new types at any time (most
// recent time: launching Nexus). Let's sanitize via convention, rather than a hard-coded list of history event types
// and their structure.
func sanitizeRecursively(m protoreflect.Message) {
m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
switch {
case fd.IsList():
// Avoid lists of non-messages
if fd.Kind() != protoreflect.MessageKind {
return true
}
list := v.List()
for i := 0; i < list.Len(); i++ {
item := list.Get(i).Message()
if isPayload(item) {
// Proto lists are homogeneous - if any items are payloads, all items are payloads
list.Truncate(0)
} else {
sanitizeRecursively(item)
}
}
case fd.IsMap():
// Avoid maps of non-messages
if fd.MapValue().Kind() != protoreflect.MessageKind {
return true
}
mapp := v.Map()
mapp.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool {
val := v.Message()
if isPayload(val) {
mapp.Clear(k)
} else {
sanitizeRecursively(val)
}
return true
})
default:
if fd.Kind() == protoreflect.MessageKind {
msg := v.Message()
if isPayload(msg) {
m.Clear(fd)
} else {
sanitizeRecursively(msg)
}
}
}
return true
})
}
func isPayload(m protoreflect.Message) bool {
fullType := string(m.Descriptor().FullName())
return strings.HasSuffix(fullType, ".Payload") || strings.HasSuffix(fullType, ".Payloads")
}
```
--------------------------------------------------------------------------------
/config.sample.yml:
--------------------------------------------------------------------------------
```yaml
temporal:
# Connection configuration
hostPort: "localhost:7233" # Local Temporal server
namespace: "default"
environment: "local" # "local" or "remote"
defaultTaskQueue: "account-transfer-queue" # Default task queue for workflows
# Connection options
timeout: "5s"
retryOptions:
initialInterval: "100ms"
maximumInterval: "10s"
maximumAttempts: 5
backoffCoefficient: 2.0
workflows:
AccountTransferWorkflow:
purpose: "Transfers money between accounts with validation and notification."
workflowIDRecipe: "transfer_{{.from_account}}_{{.to_account}}_{{.amount}}"
input:
type: "TransferInput"
fields:
- from_account: "Source account ID"
- to_account: "Destination account ID"
- amount: "Amount to transfer"
output:
type: "TransferOutput"
description: "Transfer confirmation with charge ID"
taskQueue: "account-transfer-queue"
activities:
- name: "validate"
timeout: "5s"
- name: "withdraw"
timeout: "5s"
- name: "deposit"
timeout: "5s"
- name: "sendNotification"
timeout: "5s"
- name: "undoWithdraw"
timeout: "5s"
AccountTransferWorkflowScenarios:
purpose: "Extended account transfer workflow with approval and error handling scenarios."
workflowIDRecipe: "transfer_scenario_{{.scenario_type}}_{{.from_account}}_{{.to_account}}"
input:
type: "TransferInput"
fields:
- from_account: "Source account ID"
- to_account: "Destination account ID"
- amount: "Amount to transfer"
output:
type: "TransferOutput"
description: "Transfer confirmation with charge ID"
taskQueue: "account-transfer-queue"
scenarios:
- name: "AccountTransferWorkflowRecoverableFailure"
description: "Simulates a recoverable failure scenario"
- name: "AccountTransferWorkflowHumanInLoop"
description: "Requires human approval before proceeding"
approvalTimeout: "30s"
- name: "AccountTransferWorkflowAdvancedVisibility"
description: "Includes advanced visibility features"
activities:
- name: "validate"
timeout: "5s"
- name: "withdraw"
timeout: "5s"
- name: "deposit"
timeout: "5s"
- name: "sendNotification"
timeout: "5s"
- name: "undoWithdraw"
timeout: "5s"
```
--------------------------------------------------------------------------------
/internal/config/config_test.go:
--------------------------------------------------------------------------------
```go
package config
import (
"os"
"path/filepath"
"testing"
)
func TestLoadConfig(t *testing.T) {
// Create a temporary config file
configPath := filepath.Join(t.TempDir(), "test_config.yml")
// Sample YAML content matching our struct definitions
configContent := `
temporal:
hostPort: "localhost:7233"
namespace: "default"
environment: "local"
workflows:
TestWorkflow:
purpose: "Test workflow"
input:
type: "TestRequest"
fields:
- id: "The test ID"
- name: "The test name"
- data: "Test data payload"
output:
type: "string"
description: "Test result"
taskQueue: "test-queue"
`
// Write the test config
err := os.WriteFile(configPath, []byte(configContent), 0644)
if err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
// Load the config
cfg, err := LoadConfig(configPath)
if err != nil {
t.Fatalf("Failed to load config: %v", err)
}
// Validate the loaded config
if cfg.Temporal.HostPort != "localhost:7233" {
t.Errorf("Expected HostPort to be localhost:7233, got %s", cfg.Temporal.HostPort)
}
if cfg.Temporal.Namespace != "default" {
t.Errorf("Expected Namespace to be default, got %s", cfg.Temporal.Namespace)
}
workflow, exists := cfg.Workflows["TestWorkflow"]
if !exists {
t.Fatal("TestWorkflow not found in config")
}
if workflow.Purpose != "Test workflow" {
t.Errorf("Expected workflow purpose to be 'Test workflow', got '%s'", workflow.Purpose)
}
if workflow.TaskQueue != "test-queue" {
t.Errorf("Expected task queue to be 'test-queue', got '%s'", workflow.TaskQueue)
}
if len(workflow.Input.Fields) != 3 {
t.Fatalf("Expected 3 input fields, got %d", len(workflow.Input.Fields))
}
if _, ok := workflow.Input.Fields[0]["id"]; !ok {
t.Error("Expected input field 'id' not found")
}
if _, ok := workflow.Input.Fields[1]["name"]; !ok {
t.Error("Expected input field 'name' not found")
}
if _, ok := workflow.Input.Fields[2]["data"]; !ok {
t.Error("Expected input field 'data' not found")
}
}
// TestWorkflowInputStructs verifies that workflow input configuration correctly maps to expected struct fields
func TestWorkflowInputStructs(t *testing.T) {
// Create a test workflow definition with specific input fields
wf := WorkflowDef{
Purpose: "Test input fields",
Input: ParameterDef{
Type: "TestRequest",
Fields: []map[string]string{
{"id": "The unique identifier"},
{"name": "The name field"},
{"data": "JSON payload data"},
},
},
}
// Verify input field structure
if len(wf.Input.Fields) != 3 {
t.Fatalf("Expected 3 input fields, got %d", len(wf.Input.Fields))
}
// Verify fields match expected keys
expectedFields := []string{"id", "name", "data"}
for i, expectedField := range expectedFields {
field := wf.Input.Fields[i]
found := false
for key := range field {
if key == expectedField {
found = true
break
}
}
if !found {
t.Errorf("Expected field '%s' not found at position %d", expectedField, i)
}
}
}
```
--------------------------------------------------------------------------------
/docs/VERSION_0-project.md:
--------------------------------------------------------------------------------
```markdown
# VERSION_0 Project Tasks
This document breaks down the VERSION_0 specification into small, measurable, actionable tasks.
## 1. Project Setup
- [x] Add dependencies in `go.mod`:
- `go.temporal.io/sdk`
- `gopkg.in/yaml.v3`
- `github.com/mattn/go-sqlite3`
- [x] Install dependencies via Make: `make install`
- [x] Build the application: `make build`
## 2. Config Parser
- [x] Define Go struct types in `config.go`:
- `Config`, `TemporalConfig`, `CacheConfig`, `WorkflowDef`
- [x] Implement `func LoadConfig(path string) (*Config, error)` in `config.go`
- [x] Write unit test `TestLoadConfig` in `config_test.go` using a sample YAML file
## 3. Temporal Client
- [x] Implement `func NewTemporalClient(cfg TemporalConfig) (client.Client, error)` in `temporal.go`
- [x] Write unit test `TestNewTemporalClient` with a stubbed `client.Dial`
## 4. Tool Registry
- [x] Implement workflow tool registration
- [x] Support dynamic tool definitions based on config
- [x] Add default task queue support
- [x] Write unit tests for task queue selection
## 5. MCP Protocol Handler
- [x] Implement MCP server using `mcp-golang` library
- [x] Add workflow tool registration and execution
- [x] Add system prompt registration
- [x] Implement graceful error handling for Temporal connection failures
## 6. Cache Manager
- [x] Implement `CacheClient` with methods `Get`, `Set`, and `Clear`
- [x] Initialize SQLite database with TTL and max size parameters
- [x] Write unit tests for cache functionality
## 7. ClearCache Tool
- [x] Add `ClearCache` tool definition
- [x] Implement handler for ClearCache calling `CacheClient.Clear`
- [x] Write tests for ClearCache functionality
## 8. Example Configuration
- [x] Create configuration examples (`config.yml` and `config.sample.yml`)
- [x] Add MCP configuration examples in `/examples` directory
- [x] Validate `LoadConfig` parses configuration correctly
## 9. Security & Validation
- [x] Add parameter validation for workflow tools
- [x] Implement safe error handling for failed workflows
- [x] Ensure all logging goes to stderr to avoid corrupting the JSON-RPC protocol
## 10. Performance Benchmarking
- [ ] Add benchmark `BenchmarkToolDiscovery` in `benchmarks/tool_discovery_test.go` to verify <100ms discovery
- [ ] Add benchmark `BenchmarkToolInvocation` in `benchmarks/tool_invocation_test.go` to verify <200ms invocation
## 11. Testing & CI
- [x] Add `make test` target to run all unit tests
- [x] Configure a CI workflow (e.g., GitHub Actions) to run tests on push and PR events
## 12. Documentation
- [x] Update `README.md` with project overview
- [x] Create documentation for setup and configuration instructions
- [x] Document how to run the MCP server
- [x] Document example usage with Claude in `examples/README.md`
## 13. Integration with Claude
- [x] Add example configuration for Claude Desktop in `examples/claude_config.json`
- [x] Provide comprehensive examples for temporal-mcp usage
- [x] Fix logging to ensure proper JSON-RPC communication
- [x] Update build system to use `./bin` directory for binaries
```
--------------------------------------------------------------------------------
/internal/sanitize_history_event/sanitize_history_event_test.go:
--------------------------------------------------------------------------------
```go
package sanitize_history_event
import (
"bufio"
"context"
"fmt"
"github.com/mocksi/temporal-mcp/internal/config"
"github.com/mocksi/temporal-mcp/internal/temporal"
"github.com/stretchr/testify/require"
temporal_enums "go.temporal.io/api/enums/v1"
"go.temporal.io/api/history/v1"
"google.golang.org/protobuf/encoding/protojson"
"os"
"strings"
"testing"
)
const TEST_DIR = "test_data"
const ORIGINAL_SUFFIX = "_original.jsonl"
func TestSanitizeHistoryEvent(t *testing.T) {
// To generate new test files from a real workflow history, uncomment the following line
// generateTestJson(t, "localhost:7233", "default", "someWorkflowID")
workflowIDs := make([]string, 0)
entries, err := os.ReadDir(TEST_DIR)
require.NoError(t, err)
for _, entry := range entries {
if entry.IsDir() {
continue
}
if strings.HasSuffix(entry.Name(), ORIGINAL_SUFFIX) {
workflowIDs = append(workflowIDs, entry.Name()[0:len(entry.Name())-len(ORIGINAL_SUFFIX)])
}
}
for _, workflowID := range workflowIDs {
t.Run(fmt.Sprintf("history for %s", workflowID), func(t *testing.T) {
original, sanitized := getTestFilenames(workflowID)
originalEvents := readEvents(t, original)
sanitizedEvents := readEvents(t, sanitized)
require.Equal(t, len(originalEvents), len(sanitizedEvents))
for i, actualEvent := range originalEvents {
SanitizeHistoryEvent(actualEvent)
require.Equal(t, sanitizedEvents[i], actualEvent)
}
})
}
}
func generateTestJson(t *testing.T, hostport string, namespace string, workflowID string) {
tClient, err := temporal.NewTemporalClient(config.TemporalConfig{
HostPort: hostport,
Namespace: namespace,
Environment: "local",
DefaultTaskQueue: "unused",
})
require.NoError(t, err)
iter := tClient.GetWorkflowHistory(context.Background(), workflowID, "", false, temporal_enums.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT)
original, sanitized := getTestFilenames(workflowID)
originalFile, err := os.Create(original)
require.NoError(t, err)
defer originalFile.Close()
sanitizedFile, err := os.Create(sanitized)
require.NoError(t, err)
defer sanitizedFile.Close()
for iter.HasNext() {
event, err := iter.Next()
require.NoError(t, err)
writeEvent(t, originalFile, event)
SanitizeHistoryEvent(event)
writeEvent(t, sanitizedFile, event)
}
}
func writeEvent(t *testing.T, file *os.File, event *history.HistoryEvent) {
bytes, err := protojson.Marshal(event)
require.NoError(t, err)
bytes = append(bytes, '\n')
n, err := file.Write(bytes)
require.NoError(t, err)
require.Equal(t, len(bytes), n)
}
func readEvents(t *testing.T, filename string) []*history.HistoryEvent {
f, err := os.Open(filename)
require.NoError(t, err)
defer f.Close()
var events []*history.HistoryEvent
scanner := bufio.NewScanner(f)
for scanner.Scan() {
eventJson := scanner.Text()
event := &history.HistoryEvent{}
err := protojson.Unmarshal([]byte(eventJson), event)
require.NoError(t, err)
events = append(events, event)
}
return events
}
func getTestFilenames(workflowID string) (string, string) {
original := fmt.Sprintf("%s/%s%s", TEST_DIR, workflowID, ORIGINAL_SUFFIX)
sanitized := fmt.Sprintf("%s/%s_sanitized.jsonl", TEST_DIR, workflowID)
return original, sanitized
}
```
--------------------------------------------------------------------------------
/cmd/temporal-mcp/main_test.go:
--------------------------------------------------------------------------------
```go
package main
import (
"context"
"github.com/stretchr/testify/require"
"testing"
"github.com/mocksi/temporal-mcp/internal/config"
)
// TestGetTaskQueue tests the task queue selection logic
func TestGetTaskQueue(t *testing.T) {
// Test cases to check task queue selection
tests := []struct {
name string
workflowQueue string
defaultQueue string
expectedQueue string
expectedIsDefault bool
}{
{
name: "Workflow with specific task queue",
workflowQueue: "specific-queue",
defaultQueue: "default-queue",
expectedQueue: "specific-queue",
expectedIsDefault: false,
},
{
name: "Workflow without task queue uses default",
workflowQueue: "",
defaultQueue: "default-queue",
expectedQueue: "default-queue",
expectedIsDefault: true,
},
{
name: "Empty default with empty workflow queue",
workflowQueue: "",
defaultQueue: "",
expectedQueue: "", // Empty because both are empty
expectedIsDefault: true,
},
}
// Run test cases
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup workflow and config
workflow := config.WorkflowDef{
TaskQueue: tc.workflowQueue,
}
cfg := &config.Config{
Temporal: config.TemporalConfig{
DefaultTaskQueue: tc.defaultQueue,
},
}
// Test task queue selection
taskQueue := workflow.TaskQueue
isUsingDefault := false
if taskQueue == "" {
taskQueue = cfg.Temporal.DefaultTaskQueue
isUsingDefault = true
}
// Verify results
if taskQueue != tc.expectedQueue {
t.Errorf("Expected task queue '%s', got '%s'", tc.expectedQueue, taskQueue)
}
if isUsingDefault != tc.expectedIsDefault {
t.Errorf("Expected isUsingDefault to be %v, got %v", tc.expectedIsDefault, isUsingDefault)
}
})
}
}
// TestTaskQueueOverride ensures workflow-specific task queue overrides default
func TestTaskQueueOverride(t *testing.T) {
// Setup workflow with specific queue
workflow := config.WorkflowDef{
TaskQueue: "specific-queue",
}
// Setup config with default queue
cfg := &config.Config{
Temporal: config.TemporalConfig{
DefaultTaskQueue: "default-queue",
},
}
// Check that workflow queue takes precedence
resultQueue := workflow.TaskQueue
if resultQueue != "specific-queue" {
t.Errorf("Workflow queue should be 'specific-queue', got '%s'", resultQueue)
}
// Verify it doesn't use default queue when workflow has one
if workflow.TaskQueue == "" && cfg.Temporal.DefaultTaskQueue != "" {
t.Error("Test condition error: Should not use default when workflow queue exists")
}
}
// TestDefaultTaskQueueFallback ensures default task queue is used as fallback
func TestDefaultTaskQueueFallback(t *testing.T) {
// Setup workflow without specific queue
workflow := config.WorkflowDef{
TaskQueue: "", // No task queue specified
}
// Setup config with default queue
cfg := &config.Config{
Temporal: config.TemporalConfig{
DefaultTaskQueue: "default-queue",
},
}
// Get the task queue that would be used
taskQueue := workflow.TaskQueue
if taskQueue == "" {
taskQueue = cfg.Temporal.DefaultTaskQueue
}
// Verify default queue is used
if taskQueue != "default-queue" {
t.Errorf("Should use default queue when workflow queue is empty, got '%s'", taskQueue)
}
// Verify workflow queue is actually empty
if workflow.TaskQueue != "" {
t.Errorf("Workflow queue should be empty, got '%s'", workflow.TaskQueue)
}
// Verify default queue is correctly set
if cfg.Temporal.DefaultTaskQueue != "default-queue" {
t.Errorf("Default queue should be 'default-queue', got '%s'", cfg.Temporal.DefaultTaskQueue)
}
}
// TestWorkflowInputParams tests that workflow inputs are correctly passed to ExecuteWorkflow
func TestWorkflowInputParams(t *testing.T) {
// Define test cases for different workflow input types
type TestWorkflowRequest struct {
ID string `json:"id"`
Name string `json:"name"`
Data string `json:"data"`
}
// Mock workflow parameters
testCases := []struct {
name string
workflowID string
params interface{}
}{
{
name: "Basic string parameter",
workflowID: "string-param-workflow",
params: "simple-string-value",
},
{
name: "Struct parameter",
workflowID: "struct-param-workflow",
params: TestWorkflowRequest{
ID: "test-123",
Name: "Test Workflow",
Data: "Sample payload data",
},
},
{
name: "Map parameter",
workflowID: "map-param-workflow",
params: map[string]interface{}{
"id": "map-123",
"enabled": true,
"count": 42,
"nested": map[string]string{
"key": "value",
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Recreate the workflow execution context
ctx := context.Background()
// Verify parameters are correctly structured for ExecuteWorkflow
// We can't directly test the execution but we can verify the parameters are correct
switch params := tc.params.(type) {
case string:
if params == "" {
t.Error("String parameter should not be empty")
}
case TestWorkflowRequest:
if params.ID == "" {
t.Error("Request ID should not be empty")
}
if params.Name == "" {
t.Error("Request Name should not be empty")
}
case map[string]interface{}:
if id, ok := params["id"]; !ok || id == "" {
t.Error("Map parameter should have non-empty 'id' field")
}
if nested, ok := params["nested"].(map[string]string); !ok {
t.Error("Map parameter should have valid nested map")
} else if _, ok := nested["key"]; !ok {
t.Error("Nested map should have 'key' property")
}
default:
t.Errorf("Unexpected parameter type: %T", tc.params)
}
// Verify context is valid
if ctx == nil {
t.Error("Context should not be nil")
}
})
}
}
func TestWorkflowIDComputation(t *testing.T) {
type Case struct {
recipe string
args map[string]string
expected string
}
tests := map[string]Case{
"empty": {
recipe: "",
expected: "",
},
"reference args": {
recipe: "id_{{ .one }}_{{ .two }}",
args: map[string]string{"one": "1", "two": "2"},
expected: "id_1_2",
},
"reference missing args": {
recipe: "id_{{ .one }}_{{ .missing }}",
args: map[string]string{"one": "1"},
expected: "id_1_<no value>",
},
"hash all args by accident": {
recipe: "id_{{ hash }}",
args: map[string]string{"one": "1", "two": "2"},
expected: "id_3822076040",
},
"hash all args properly": {
recipe: "id_{{ hash . }}",
args: map[string]string{"one": "1", "two": "2"},
expected: "id_3822076040",
},
"hash some args": {
recipe: "id_{{ hash .one .two }}",
args: map[string]string{"one": "1", "two": "2"},
expected: "id_1475351198",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
def := config.WorkflowDef{
WorkflowIDRecipe: tc.recipe,
}
actual, err := computeWorkflowID(def, tc.args)
require.NoError(t, err)
require.Equal(t, tc.expected, actual)
})
}
}
```
--------------------------------------------------------------------------------
/internal/temporal/client_test.go:
--------------------------------------------------------------------------------
```go
package temporal
import (
"context"
"strings"
"testing"
"time"
"github.com/mocksi/temporal-mcp/internal/config"
"go.temporal.io/sdk/client"
)
// TestNewTemporalClient tests the client creation with different configurations
func TestNewTemporalClient(t *testing.T) {
// Test valid local configuration
t.Run("ValidLocalConfig", func(t *testing.T) {
// Use a non-standard port to ensure we won't accidentally connect to a real server
cfg := config.TemporalConfig{
HostPort: "localhost:12345", // Use a port that's unlikely to have a Temporal server
Namespace: "default",
Environment: "local",
Timeout: "5s",
}
// Attempt to create client - we expect a connection error, not a config error
client, err := NewTemporalClient(cfg)
// Check that either:
// 1. We got a connection error (most likely case)
// 2. Or somehow we got a valid client (unlikely, but possible if a test server is running)
if err != nil {
// Verify this is a connection error, not a config validation error
if !strings.Contains(err.Error(), "failed to create Temporal client") {
t.Errorf("Expected connection error, got: %v", err)
}
} else {
// If we got a client, make sure to close it
defer client.Close()
}
})
// Test invalid environment
t.Run("InvalidEnvironment", func(t *testing.T) {
cfg := config.TemporalConfig{
HostPort: "localhost:7233",
Namespace: "default",
Environment: "invalid",
Timeout: "5s",
}
_, err := NewTemporalClient(cfg)
if err == nil {
t.Error("Expected error for invalid environment, got nil")
}
})
// Test invalid timeout
t.Run("InvalidTimeout", func(t *testing.T) {
cfg := config.TemporalConfig{
HostPort: "localhost:7233",
Namespace: "default",
Environment: "local",
Timeout: "invalid",
}
_, err := NewTemporalClient(cfg)
if err == nil {
t.Error("Expected error for invalid timeout, got nil")
}
})
// Test remote environment (which is not implemented yet)
t.Run("RemoteEnvironment", func(t *testing.T) {
cfg := config.TemporalConfig{
HostPort: "test.tmprl.cloud:7233",
Namespace: "test-namespace",
Environment: "remote",
}
_, err := NewTemporalClient(cfg)
if err == nil {
t.Error("Expected error for unimplemented remote environment, got nil")
}
})
}
// MockWorkflowClient is a mock implementation of the Temporal client for testing
type MockWorkflowClient struct {
lastWorkflowName string
lastParams interface{}
lastOptions client.StartWorkflowOptions
}
// ExecuteWorkflow mocks the ExecuteWorkflow method for testing
func (m *MockWorkflowClient) ExecuteWorkflow(ctx context.Context, options client.StartWorkflowOptions, workflow interface{}, args ...interface{}) (client.WorkflowRun, error) {
m.lastWorkflowName = workflow.(string)
m.lastOptions = options
if len(args) > 0 {
m.lastParams = args[0]
}
// Return a mock workflow run
return &MockWorkflowRun{}, nil
}
// Close is a no-op for the mock client
func (m *MockWorkflowClient) Close() {}
// MockWorkflowRun is a mock implementation of WorkflowRun for testing
type MockWorkflowRun struct{}
// GetID returns a mock workflow ID
func (m *MockWorkflowRun) GetID() string {
return "mock-workflow-id"
}
// GetRunID returns a mock run ID
func (m *MockWorkflowRun) GetRunID() string {
return "mock-run-id"
}
// Get is a mock implementation that returns no error
func (m *MockWorkflowRun) Get(ctx context.Context, valuePtr interface{}) error {
return nil
}
// GetWithOptions is a mock implementation of the WorkflowRun interface method
func (m *MockWorkflowRun) GetWithOptions(ctx context.Context, valuePtr interface{}, opts client.WorkflowRunGetOptions) error {
return nil
}
// TestWorkflowExecution tests workflow execution with different types of input parameters
func TestWorkflowExecution(t *testing.T) {
// Define test structs
type TestRequest struct {
ID string `json:"id"`
Value string `json:"value"`
}
type ComplexRequest struct {
ClientID string `json:"client_id"`
Command string `json:"command"`
Data map[string]interface{} `json:"data"`
Timestamp time.Time `json:"timestamp"`
}
// Test cases with different input types
testCases := []struct {
name string
workflowName string
taskQueue string
params interface{}
expectedParams interface{}
}{
{
name: "String Parameter",
workflowName: "string-workflow",
taskQueue: "default-queue",
params: "simple-string-input",
expectedParams: "simple-string-input",
},
{
name: "Struct Parameter",
workflowName: "struct-workflow",
taskQueue: "test-queue",
params: TestRequest{
ID: "req-123",
Value: "test-value",
},
expectedParams: TestRequest{
ID: "req-123",
Value: "test-value",
},
},
{
name: "Complex Parameter",
workflowName: "complex-workflow",
taskQueue: "complex-queue",
params: ComplexRequest{
ClientID: "client-456",
Command: "analyze",
Data: map[string]interface{}{"key": "value"},
Timestamp: time.Now(),
},
expectedParams: ComplexRequest{
ClientID: "client-456",
Command: "analyze",
Data: map[string]interface{}{"key": "value"},
// Time will be different but type should match
},
},
{
name: "Map Parameter",
workflowName: "map-workflow",
taskQueue: "map-queue",
params: map[string]interface{}{
"id": "map-789",
"count": 42,
"active": true,
},
expectedParams: map[string]interface{}{
"id": "map-789",
"count": 42,
"active": true,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create a mock client
mockClient := &MockWorkflowClient{}
// Execute the workflow with the test parameters
ctx := context.Background()
options := client.StartWorkflowOptions{
ID: "test-" + tc.workflowName,
TaskQueue: tc.taskQueue,
}
// Call ExecuteWorkflow on the mock client
_, err := mockClient.ExecuteWorkflow(ctx, options, tc.workflowName, tc.params)
if err != nil {
t.Fatalf("ExecuteWorkflow failed: %v", err)
}
// Verify workflow name
if mockClient.lastWorkflowName != tc.workflowName {
t.Errorf("Expected workflow name %s, got %s", tc.workflowName, mockClient.lastWorkflowName)
}
// Verify task queue
if mockClient.lastOptions.TaskQueue != tc.taskQueue {
t.Errorf("Expected task queue %s, got %s", tc.taskQueue, mockClient.lastOptions.TaskQueue)
}
// Verify parameters were passed correctly
switch params := mockClient.lastParams.(type) {
case string:
expectedStr, ok := tc.expectedParams.(string)
if !ok || params != expectedStr {
t.Errorf("Expected string param %v, got %v", tc.expectedParams, params)
}
case TestRequest:
expected, ok := tc.expectedParams.(TestRequest)
if !ok || params.ID != expected.ID || params.Value != expected.Value {
t.Errorf("Expected struct param %v, got %v", tc.expectedParams, params)
}
case ComplexRequest:
expected, ok := tc.expectedParams.(ComplexRequest)
if !ok || params.ClientID != expected.ClientID || params.Command != expected.Command {
t.Errorf("Expected complex param %v, got %v", tc.expectedParams, params)
}
case map[string]interface{}:
expected, ok := tc.expectedParams.(map[string]interface{})
if !ok {
t.Errorf("Expected map param %v, got %v", tc.expectedParams, params)
}
// Check key values
for k, v := range expected {
if params[k] != v {
t.Errorf("Expected map[%s]=%v, got %v", k, v, params[k])
}
}
default:
t.Errorf("Unexpected parameter type: %T", params)
}
})
}
}
```
--------------------------------------------------------------------------------
/docs/temporal.md:
--------------------------------------------------------------------------------
```markdown
# Starting and Getting Responses from Temporal Workflows in Go: A Developer's Guide
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.
## Introduction to Temporal
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].
## Setting Up Your Environment
### Installing the Temporal Go SDK
```bash
go get go.temporal.io/sdk
```
### Connecting to Temporal Service
```go
import (
"go.temporal.io/sdk/client"
)
// Create a Temporal Client to communicate with the Temporal Service
temporalClient, err := client.Dial(client.Options{
HostPort: client.DefaultHostPort, // Defaults to "127.0.0.1:7233"
})
if err != nil {
log.Fatalln("Unable to create Temporal Client", err)
}
defer temporalClient.Close()
```
For Temporal Cloud connections:
```go
// For Temporal Cloud
temporalClient, err := client.Dial(client.Options{
HostPort: "your-namespace.tmprl.cloud:7233",
Namespace: "your-namespace",
ConnectionOptions: client.ConnectionOptions{
TLS: &tls.Config{},
},
})
```
## Defining a Simple Workflow
```go
import (
"time"
"go.temporal.io/sdk/workflow"
)
// Define your workflow function
func GreetingWorkflow(ctx workflow.Context, name string) (string, error) {
// Set activity options
ao := workflow.ActivityOptions{
TaskQueue: "greeting-tasks",
StartToCloseTimeout: time.Minute,
ScheduleToCloseTimeout: time.Minute,
}
ctx = workflow.WithActivityOptions(ctx, ao)
// Execute activity and get result
var result string
err := workflow.ExecuteActivity(ctx, GreetingActivity, name).Get(ctx, &result)
if err != nil {
return "", err
}
return result, nil
}
// Define your activity function
func GreetingActivity(ctx context.Context, name string) (string, error) {
return "Hello, " + name + "!", nil
}
```
## Creating a Worker
```go
import (
"go.temporal.io/sdk/worker"
)
func startWorker(c client.Client) {
// Create worker options
w := worker.New(c, "greeting-tasks", worker.Options{})
// Register workflow and activity with the worker
w.RegisterWorkflow(GreetingWorkflow)
w.RegisterActivity(GreetingActivity)
// Start the worker
err := w.Run(worker.InterruptCh())
if err != nil {
log.Fatalln("Unable to start worker", err)
}
}
```
## Starting Workflow Executions
```go
// Define workflow options
workflowOptions := client.StartWorkflowOptions{
ID: "greeting-workflow-" + uuid.New().String(),
TaskQueue: "greeting-tasks",
}
// Start the workflow execution
workflowRun, err := temporalClient.ExecuteWorkflow(
context.Background(),
workflowOptions,
GreetingWorkflow,
"Temporal Developer"
)
if err != nil {
log.Fatalln("Unable to execute workflow", err)
}
// Get workflow ID and run ID for future reference
fmt.Printf("Started workflow: WorkflowID: %s, RunID: %s\n",
workflowRun.GetID(),
workflowRun.GetRunID())
```
## Getting Responses from Workflow Executions
### 1. Synchronous Response
```go
// Wait for workflow completion and get result
var result string
err = workflowRun.Get(context.Background(), &result)
if err != nil {
log.Fatalln("Unable to get workflow result", err)
}
fmt.Printf("Workflow result: %s\n", result)
```
### 2. Retrieving Results Later
```go
// Get workflow result using workflow ID and run ID
workflowID := "greeting-workflow-123"
runID := "run-id-456"
// Retrieve the workflow handle
workflowRun = temporalClient.GetWorkflow(context.Background(), workflowID, runID)
// Get the result
var result string
err = workflowRun.Get(context.Background(), &result)
if err != nil {
log.Fatalln("Unable to get workflow result", err)
}
```
### 3. Using Queries to Get Workflow State
```go
// Define query handler in your workflow
func GreetingWorkflow(ctx workflow.Context, name string) (string, error) {
// Set up state variable
greeting := ""
// Register query handler
err := workflow.SetQueryHandler(ctx, "getGreeting", func() (string, error) {
return greeting, nil
})
if err != nil {
return "", err
}
// Workflow logic...
greeting = "Hello, " + name + "!"
return greeting, nil
}
// Query the workflow state from client
response, err := temporalClient.QueryWorkflow(context.Background(),
workflowID, runID, "getGreeting")
if err != nil {
log.Fatalln("Unable to query workflow", err)
}
var greeting string
err = response.Get(&greeting)
if err != nil {
log.Fatalln("Unable to decode query result", err)
}
fmt.Printf("Current greeting: %s\n", greeting)
```
### 4. Message Passing with Signals
```go
// In your workflow, set up a signal channel
func GreetingWorkflow(ctx workflow.Context, name string) (string, error) {
// Create signal channel
updateNameChannel := workflow.GetSignalChannel(ctx, "update_name")
for {
// Wait for signal or timeout
selector := workflow.NewSelector(ctx)
selector.AddReceive(updateNameChannel, func(c workflow.ReceiveChannel, more bool) {
var newName string
c.Receive(ctx, &newName)
name = newName
// Process updated name...
})
// Add timeout to exit workflow
selector.Select(ctx)
}
}
// Send signal to workflow
err = temporalClient.SignalWorkflow(context.Background(),
workflowID, runID, "update_name", "New Name")
if err != nil {
log.Fatalln("Unable to signal workflow", err)
}
```
## Error Handling and Retries
```go
// Configure retry policy
retryPolicy := &temporal.RetryPolicy{
InitialInterval: time.Second,
BackoffCoefficient: 2.0,
MaximumInterval: time.Minute * 5,
MaximumAttempts: 5,
}
// Apply retry policy to activity options
ao := workflow.ActivityOptions{
TaskQueue: "greeting-tasks",
StartToCloseTimeout: time.Minute,
ScheduleToCloseTimeout: time.Minute,
RetryPolicy: retryPolicy,
}
```
## Getting Workflow Information
```go
// Inside a workflow, get workflow execution info
info := workflow.GetInfo(ctx)
workflowID := info.WorkflowExecution.ID
runID := info.WorkflowExecution.RunID[7]
```
## Workflow Run ID
To get the current run ID within a workflow (useful for self-termination)[7]:
```go
// Inside a workflow
runID := workflow.GetInfo(ctx).WorkflowExecution.RunID
```
## Conclusion
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].
Remember that Temporal is particularly valuable for scenarios involving:
- Long-running, potentially multi-step processes
- Coordination between multiple services
- Processes requiring automatic retries
- Workflows that need to maintain state even through system failures[14]
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.
Sources
[1] Go SDK developer guide | Temporal Platform Documentation https://docs.temporal.io/develop/go
[2] temporal - Go Packages https://pkg.go.dev/go.temporal.io/sdk/temporal
[3] Workflow message passing - Go SDK - Temporal Docs https://docs.temporal.io/develop/go/message-passing
[4] temporalio/sdk-go: Temporal Go SDK - GitHub https://github.com/temporalio/sdk-go
[5] Temporal Client - Go SDK https://docs.temporal.io/develop/go/temporal-clients
[6] README.md - Temporal Go SDK samples - GitHub https://github.com/temporalio/samples-go/blob/main/README.md
[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
[8] Run your first Temporal application with the Go SDK https://learn.temporal.io/getting_started/go/first_program_in_go/
[9] Go SDK developer guide | Temporal Platform Documentation https://docs.temporal.io/develop/go/
[10] Build a Temporal Application from scratch in Go https://learn.temporal.io/getting_started/go/hello_world_in_go/
[11] workflow package - go.temporal.io/sdk/workflow - Go Packages https://pkg.go.dev/go.temporal.io/sdk/workflow
[12] temporalio/samples-go: Temporal Go SDK samples - GitHub https://github.com/temporalio/samples-go
[13] workflow - Go Packages https://pkg.go.dev/go.temporal.io/temporal/workflow
[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/
[15] workflowcheck command - go.temporal.io/sdk/contrib/tools ... https://pkg.go.dev/go.temporal.io/sdk/contrib/tools/workflowcheck
[16] Implementing Temporal IO in Golang Microservices Architecture https://www.softwareletters.com/p/implementing-temporal-io-golang-microservices-architecture-stepbystep-guide
[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/
[18] Temporal Workflow | Temporal Platform Documentation https://docs.temporal.io/workflows
[19] Intro to Temporal with Go SDK - YouTube https://www.youtube.com/watch?v=-KWutSkFda8
[20] Temporal SDK : r/golang - Reddit https://www.reddit.com/r/golang/comments/15kwzke/temporal_sdk/
[21] Core application - Go SDK | Temporal Platform Documentation https://docs.temporal.io/develop/go/core-application
[22] Get started with Temporal and Go https://learn.temporal.io/getting_started/go/
[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
[24] Workflow with Temporal - Capten.AI https://capten.ai/learning-center/10-learn-temporal/understand-temporal-workflow/workflow/
[25] client package - go.temporal.io/sdk/client - Go Packages https://pkg.go.dev/go.temporal.io/sdk/client
[26] documentation-samples-go/yourapp/your_workflow_definition_dacx ... https://github.com/temporalio/documentation-samples-go/blob/main/yourapp/your_workflow_definition_dacx.go
```
--------------------------------------------------------------------------------
/docs/VERSION_0.md:
--------------------------------------------------------------------------------
```markdown
# Temporal MCP Technical Specification
## 1. Introduction
### Purpose and Goals
The primary purpose of this project is to create a Model Control Protocol (MCP) server implementation in Go that:
- Reads a YAML configuration defining Temporal workflows
- Automatically exposes each workflow as an MCP tool
- Handles tool invocation requests from MCP clients
- Executes workflows via Temporal and returns results
### Scope of Implementation
This specification covers the development of an MCP-compliant server that:
- Parses workflow definitions from YAML configuration
- Connects to a Temporal service using the Go SDK
- Dynamically generates MCP tool definitions based on the configured workflows
- Handles tool invocation lifecycle and result retrieval
- Implements caching with cache invalidation capabilities
### Definitions and Acronyms
- **MCP**: Model Control Protocol - A protocol for communication between AI models and external tools
- **Temporal**: A distributed, scalable workflow orchestration engine
- **Workflow**: A durable function that orchestrates activities in Temporal
- **YAML**: YAML Ain't Markup Language - A human-friendly data serialization standard
## 2. System Overview
### Client-Host-Server Architecture
```
+----------------+ +----------------+ +----------------+
| MCP Client |<--->| MCP Server |<--->| Temporal |
| (AI Assistant) | | (Go Service) | | Service |
+----------------+ +----------------+ +----------------+
^ ^
| |
v v
+----------------+ +----------------+
| YAML Workflow | | SQLite Cache |
| Configuration | | Database |
+----------------+ +----------------+
```
### Server Components
1. **Config Parser**: Loads and parses YAML workflow definitions
2. **MCP Protocol Handler**: Manages MCP message processing
3. **Temporal Client**: Interfaces with Temporal service
4. **Tool Registry**: Dynamically generates tool definitions from workflows
5. **Cache Manager**: Handles SQLite caching for workflow results
## 3. MCP Protocol Implementation
### 3.1 Protocol Definition
- Compliant with MCP Specification v0.1
- JSON-based message exchange
- Request/response communication pattern
### 3.2 Message Format
#### Tool Definitions Format
Tools are dynamically generated from workflow definitions in the YAML configuration, with an additional tool for cache management.
## 4. Server Implementation
### Core Components
#### YAML Configuration Schema
```go
type Config struct {
Temporal TemporalConfig `yaml:"temporal"`
Workflows map[string]WorkflowDef `yaml:"workflows"`
Cache CacheConfig `yaml:"cache"`
}
type TemporalConfig struct {
// Connection configuration
HostPort string `yaml:"hostPort"`
Namespace string `yaml:"namespace"`
// Environment type
Environment string `yaml:"environment"` // "local" or "remote"
// Authentication (for remote)
Auth *AuthConfig `yaml:"auth,omitempty"`
// TLS configuration (for remote)
TLS *TLSConfig `yaml:"tls,omitempty"`
// Connection options
RetryOptions *RetryConfig `yaml:"retryOptions,omitempty"`
Timeout string `yaml:"timeout,omitempty"`
}
type CacheConfig struct {
Enabled bool `yaml:"enabled"`
DatabasePath string `yaml:"databasePath"`
TTL string `yaml:"ttl"` // Time-to-live for cached results
MaxCacheSize int64 `yaml:"maxCacheSize"` // Maximum size in bytes
CleanupInterval string `yaml:"cleanupInterval"` // How often to clean expired entries
}
```
## 5. Cache Implementation
### Cache Clear Tool
The server implements a special tool for cache management:
```json
{
"name": "ClearCache",
"description": "Clears cached workflow results, either by specific workflow or the entire cache.",
"parameters": {
"type": "object",
"properties": {
"workflowName": {
"type": "string",
"description": "Optional. Name of the workflow to clear the cache for. If not provided, all cache entries will be cleared."
}
},
"required": []
}
}
```
### Cache Manager with Clear Function
```go
func (cm *CacheManager) Clear(workflowName string) (int64, error) {
if !cm.enabled {
return 0, nil
}
var result sql.Result
var err error
if workflowName == "" {
// Clear entire cache
result, err = cm.db.Exec("DELETE FROM workflow_cache")
} else {
// Clear cache for specific workflow
result, err = cm.db.Exec(
"DELETE FROM workflow_cache WHERE workflow_name = ?",
workflowName,
)
}
if err != nil {
return 0, fmt.Errorf("failed to clear cache: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return 0, fmt.Errorf("failed to get rows affected: %w", err)
}
return rowsAffected, nil
}
```
### Clear Cache Tool Handler
```go
func (s *MCPServer) handleClearCache(params map[string]interface{}) (ToolCallResponse, error) {
// Extract workflow name if provided
var workflowName string
if name, ok := params["workflowName"].(string); ok {
workflowName = name
}
// Clear cache
rowsAffected, err := s.cacheManager.Clear(workflowName)
if err != nil {
return ToolCallResponse{}, fmt.Errorf("failed to clear cache: %w", err)
}
// Create response
response := ToolCallResponse{
ToolName: "ClearCache",
Status: "completed",
Result: map[string]interface{}{
"success": true,
"entriesCleared": rowsAffected,
"workflow": workflowName,
},
}
return response, nil
}
```
### Tool Registration Integration
```go
func (s *MCPServer) generateToolDefinitions() error {
// Generate tools for workflows
for name, workflow := range s.config.Workflows {
// Create tool definition for workflow
// ...existing code...
}
// Add special tool for clearing cache
s.tools["ClearCache"] = ToolDefinition{
Name: "ClearCache",
Description: "Clears cached workflow results, either by specific workflow or the entire cache.",
Parameters: JSONSchema{
Type: "object",
Properties: map[string]JSONSchemaProperty{
"workflowName": {
Type: "string",
Description: "Optional. Name of the workflow to clear the cache for. If not provided, all cache entries will be cleared.",
},
},
Required: []string{},
},
Internal: true,
}
return nil
}
```
### Enhanced Tool Invocation Router
```go
func (s *MCPServer) handleToolCall(call ToolCallRequest) (ToolCallResponse, error) {
toolName := call.Name
tool, exists := s.tools[toolName]
if !exists {
return ToolCallResponse{}, fmt.Errorf("tool %s not found", toolName)
}
// Handle special internal tools
if tool.Internal {
switch toolName {
case "ClearCache":
return s.handleClearCache(call.Parameters)
default:
return ToolCallResponse{}, fmt.Errorf("unknown internal tool: %s", toolName)
}
}
// Regular workflow tool handling
// ...existing workflow execution code...
}
```
## 6. Configuration Example
```yaml
temporal:
# Connection configuration
hostPort: "localhost:7233" # Local Temporal server
namespace: "default"
environment: "local" # "local" or "remote"
# Connection options
timeout: "5s"
retryOptions:
initialInterval: "100ms"
maximumInterval: "10s"
maximumAttempts: 5
backoffCoefficient: 2.0
# For remote Temporal server (Temporal Cloud)
# environment: "remote"
# hostPort: "your-namespace.tmprl.cloud:7233"
# namespace: "your-namespace"
# auth:
# clientID: "your-client-id"
# clientSecret: "your-client-secret"
# audience: "your-audience"
# oauth2URL: "https://auth.temporal.io/oauth2/token"
# tls:
# certPath: "/path/to/client.pem"
# keyPath: "/path/to/client.key"
# caPath: "/path/to/ca.pem"
# serverName: "*.tmprl.cloud"
# insecureSkipVerify: false
# Cache configuration
cache:
enabled: true
databasePath: "./workflow_cache.db"
ttl: "24h" # Cache entries expire after 24 hours
maxCacheSize: 104857600 # 100MB max cache size
cleanupInterval: "1h" # Run cleanup every hour
workflows:
IngestWorkflow:
purpose: "Ingests documents into the vector store."
input:
type: "IngestRequest"
fields:
- doc_id: "The document ID to ingest."
output:
type: "string"
description: "ID of the ingested document."
taskQueue: "ingest-queue"
UpdateXHRWorkflow:
purpose: "Updates XHR requests for DOM elements."
input:
type: "RAGRequest"
fields:
- session_id: "The session ID associated with the request."
- action: "The action to perform on the DOM element."
output:
type: "RAGResponse"
description: "The result of processing the fetched data."
taskQueue: "xhr-queue"
ChatWorkflow:
purpose: "Processes chat completion requests."
input:
type: "ChatRequest"
fields:
- id: "The ID of the chat request."
- prompt_id: "The prompt ID for the chat."
output:
type: "string"
description: "The string completion response."
taskQueue: "chat-queue"
NewChatMessageWorkflow:
purpose: "Processes new chat messages and updates JSON."
input:
type: "Message"
fields:
- client_id: "The client ID associated with the message."
- timestamp: "The timestamp of the message."
- content: "The content of the message."
- updated_json: "The updated JSON content."
output:
type: "Message"
description: "The message with explanation and updated JSON."
taskQueue: "message-queue"
ProxieJSONWorkflow:
purpose: "Processes JSON payloads from Proxie."
input:
type: "ProxieJSONRequest"
fields:
- client_id: "The client ID associated with the request."
- content: "The content of the request."
- updated_json: "The updated JSON content."
- request_hash: "The request hash for tracking."
output:
type: "string"
description: "JSON string response for Proxie."
taskQueue: "json-queue"
```
## 7. Security Considerations
### Data Protection
- No persistent storage of sensitive workflow data in MCP server
- TLS for Temporal Cloud connections
- Secure parameter handling
### Validation
- Input validation against schema before workflow execution
- Configuration validation at startup
- Response validation before returning to client
## 8. Performance Requirements
### Scalability
- Support for multiple concurrent tool invocations
- Efficient type conversion and serialization
- Minimal memory footprint
### Latency
- Tool discovery response < 100ms
- Tool invocation initialization < 200ms
- Cache hits < 10ms
## 9. Testing Strategy
### Unit Testing
- Configuration parsing
- Tool definition generation
- Cache operations
- MCP message handling
### Integration Testing
- End-to-end workflow execution
- Cache hit/miss scenarios
- Cache clearing functionality
## 10. Future Enhancements
### Roadmap
1. **VERSION_0 (Initial Release)**:
- Basic YAML configuration parsing
- Dynamic tool definition generation
- Temporal workflow integration
- SQLite caching with clear functionality
- Stdio transport for MCP
2. **VERSION_1**:
- Enhanced type system with automatic struct generation
- HTTP/SSE transport support
- Advanced cache analytics
- Improved error handling and reporting
3. **VERSION_2**:
- Advanced workflow querying capabilities
- Metrics and monitoring integration
- Hot reloading of configuration
- Cloud-native deployment options
## Conclusion
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.
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
package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"
"github.com/google/uuid"
"github.com/mocksi/temporal-mcp/internal/sanitize_history_event"
"google.golang.org/protobuf/encoding/protojson"
"text/template"
mcp "github.com/metoro-io/mcp-golang"
"github.com/metoro-io/mcp-golang/transport/stdio"
"github.com/mocksi/temporal-mcp/internal/config"
"github.com/mocksi/temporal-mcp/internal/temporal"
temporal_enums "go.temporal.io/api/enums/v1"
"go.temporal.io/sdk/client"
)
func main() {
// Parse command line arguments
configFile := flag.String("config", "config.yml", "Path to configuration file")
flag.Parse()
// CRITICAL: Configure all loggers to write to stderr instead of stdout
// This is essential as any output to stdout will corrupt the JSON-RPC stream
log.SetOutput(os.Stderr)
log.Println("Starting Temporal MCP server...")
// Setup signal handling for graceful shutdown
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
// Load configuration
cfg, err := config.LoadConfig(*configFile)
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}
log.Printf("Loaded configuration with %d workflows", len(cfg.Workflows))
// Initialize Temporal client
var temporalClient client.Client
var temporalError error
temporalClient, temporalError = temporal.NewTemporalClient(cfg.Temporal)
if temporalError != nil {
log.Printf("WARNING: Failed to connect to Temporal service: %v", temporalError)
log.Printf("MCP will run in degraded mode - workflow executions will return errors")
} else {
defer temporalClient.Close()
log.Printf("Connected to Temporal service at %s", cfg.Temporal.HostPort)
}
// Create a new MCP server with stdio transport for AI model communication
server := mcp.NewServer(stdio.NewStdioServerTransport())
// Create tool registry - used in future enhancements
// registry := tool.NewRegistry(cfg, temporalClient, cacheClient)
// Register all workflow tools
log.Println("Registering workflow tools...")
err = registerWorkflowTools(server, cfg, temporalClient)
if err != nil {
log.Fatalf("Failed to register workflow tools: %v", err)
}
// Register get workflow history tool
err = registerGetWorkflowHistoryTool(server, temporalClient)
if err != nil {
log.Fatalf("Failed to register get workflow history tool: %v", err)
}
// Register system prompt
err = registerSystemPrompt(server, cfg)
if err != nil {
log.Fatalf("Failed to register system prompt: %v", err)
}
// Start the MCP server in a goroutine
go func() {
log.Printf("Temporal MCP server is running. Press Ctrl+C to stop.")
if err := server.Serve(); err != nil {
log.Fatalf("Server error: %v", err)
}
}()
// Wait for termination signal
sig := <-sigCh
log.Printf("Received signal %v, shutting down MCP server...", sig)
log.Printf("Temporal MCP server has been stopped.")
}
// registerWorkflowTools registers all workflow definitions as MCP tools
func registerWorkflowTools(server *mcp.Server, cfg *config.Config, tempClient client.Client) error {
// Register all workflows as tools
for name, workflow := range cfg.Workflows {
err := registerWorkflowTool(server, name, workflow, tempClient, cfg)
if err != nil {
return fmt.Errorf("failed to register workflow tool %s: %w", name, err)
}
log.Printf("Registered workflow tool: %s", name)
}
return nil
}
// registerWorkflowTool registers a single workflow as an MCP tool
func registerWorkflowTool(server *mcp.Server, name string, workflow config.WorkflowDef, tempClient client.Client, cfg *config.Config) error {
// Define the type for workflow parameters based on fields
type WorkflowParams struct {
Params map[string]string `json:"params"`
ForceRerun bool `json:"force_rerun"`
}
// Build detailed parameter descriptions for tool registration
paramDescriptions := "\n\n**Parameters:**\n"
for _, field := range workflow.Input.Fields {
for fieldName, description := range field {
isRequired := !strings.Contains(description, "Optional")
if isRequired {
paramDescriptions += fmt.Sprintf("- `%s` (required): %s\n", fieldName, description)
} else {
paramDescriptions += fmt.Sprintf("- `%s` (optional): %s\n", fieldName, description)
}
}
}
// Add example usage
paramDescriptions += "\n**Example Usage:**\n```json\n{\n \"params\": {\n"
paramExamples := []string{}
for _, field := range workflow.Input.Fields {
for fieldName, _ := range field {
if strings.Contains(fieldName, "json") {
paramExamples = append(paramExamples, fmt.Sprintf(" \"%s\": {\"example\": \"value\"}", fieldName))
} else if strings.Contains(fieldName, "id") {
paramExamples = append(paramExamples, fmt.Sprintf(" \"%s\": \"example-id-123\"", fieldName))
} else {
paramExamples = append(paramExamples, fmt.Sprintf(" \"%s\": \"example value\"", fieldName))
}
}
}
paramDescriptions += strings.Join(paramExamples, ",\n")
paramDescriptions += "\n },\n \"force_rerun\": false\n}\n```"
// Create complete extended purpose description
extendedPurpose := workflow.Purpose + paramDescriptions
// Register the tool with MCP server
return server.RegisterTool(name, extendedPurpose, func(args WorkflowParams) (*mcp.ToolResponse, error) {
// Check if Temporal client is available
if tempClient == nil {
log.Printf("Error: Temporal client is not available for workflow: %s", name)
return mcp.NewToolResponse(mcp.NewTextContent(
"Error: Temporal service is currently unavailable. Please try again later.",
)), nil
}
// Validate required parameters before execution
if args.Params == nil {
return mcp.NewToolResponse(mcp.NewTextContent(
fmt.Sprintf("Error: No parameters provided for workflow %s. Please provide required parameters.", name),
)), nil
}
// Build list of required parameters
var requiredParams []string
for _, field := range workflow.Input.Fields {
for fieldName, description := range field {
if !strings.Contains(description, "Optional") {
requiredParams = append(requiredParams, fieldName)
}
}
}
// Check for missing required parameters
var missingParams []string
for _, param := range requiredParams {
if _, exists := args.Params[param]; !exists || args.Params[param] == "" {
missingParams = append(missingParams, param)
}
}
// Return error if any required parameters are missing
if len(missingParams) > 0 {
missingParamsList := strings.Join(missingParams, ", ")
return mcp.NewToolResponse(mcp.NewTextContent(
fmt.Sprintf("Error: Missing required parameters for workflow %s: %s", name, missingParamsList),
)), nil
}
// Execute the workflow
// Determine which task queue to use (workflow-specific or default)
taskQueue := workflow.TaskQueue
if taskQueue == "" && cfg != nil {
taskQueue = cfg.Temporal.DefaultTaskQueue
log.Printf("Using default task queue: %s for workflow %s", taskQueue, name)
}
workflowID, err := computeWorkflowID(workflow, args.Params)
if err != nil {
log.Printf("Error computing workflow ID from arguments: %v", err)
return mcp.NewToolResponse(mcp.NewTextContent(
fmt.Sprintf("Error computing workflow ID from arguments: %v", err),
)), nil
}
if workflowID == "" {
log.Printf("Workflow %q has an empty or missing workflowIDRecipe - using a random workflow id", name)
workflowID = uuid.NewString()
}
// This will execute a new workflow when:
// - there is no workflow with the given id
// - there is a failed workflow with the given id (e.g. terminated, failed, timed out)
// and attach to an existing workflow when:
// - there is a running workflow with the given id
// - there is a successful workflow with the given id
//
// Note that temporal's data retention window (a setting on each namespace) influences the behavior above
reusePolicy := temporal_enums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY
conflictPolicy := temporal_enums.WORKFLOW_ID_CONFLICT_POLICY_USE_EXISTING
if args.ForceRerun {
// This will execute a new workflow in all cases. If there is a running workflow with the given id, it will
// be terminated.
reusePolicy = temporal_enums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE
conflictPolicy = temporal_enums.WORKFLOW_ID_CONFLICT_POLICY_TERMINATE_EXISTING
}
wfOptions := client.StartWorkflowOptions{
TaskQueue: taskQueue,
ID: workflowID,
WorkflowIDReusePolicy: reusePolicy,
WorkflowIDConflictPolicy: conflictPolicy,
}
log.Printf("Starting workflow %s on task queue %s", name, taskQueue)
// Start workflow execution
run, err := tempClient.ExecuteWorkflow(context.Background(), wfOptions, name, args.Params)
if err != nil {
log.Printf("Error starting workflow %s: %v", name, err)
return mcp.NewToolResponse(mcp.NewTextContent(
fmt.Sprintf("Error executing workflow: %v", err),
)), nil
}
log.Printf("Workflow started: WorkflowID=%s RunID=%s", run.GetID(), run.GetRunID())
// Wait for workflow completion
var result string
if err := run.Get(context.Background(), &result); err != nil {
log.Printf("Error in workflow %s execution: %v", name, err)
return mcp.NewToolResponse(mcp.NewTextContent(
fmt.Sprintf("Workflow failed: %v", err),
)), nil
}
log.Printf("Workflow %s completed successfully", name)
return mcp.NewToolResponse(mcp.NewTextContent(result)), nil
})
}
func computeWorkflowID(workflow config.WorkflowDef, params map[string]string) (string, error) {
tmpl := template.New("id_recipe")
tmpl.Funcs(template.FuncMap{
"hash": func(paramsToHash ...any) (string, error) {
return hashWorkflowArgs(params, paramsToHash...)
},
})
if _, err := tmpl.Parse(workflow.WorkflowIDRecipe); err != nil {
return "", err
}
writer := strings.Builder{}
if err := tmpl.Execute(&writer, params); err != nil {
return "", err
}
return writer.String(), nil
}
// registerGetWorkflowHistoryTool registres a tool that gets workflow histories
func registerGetWorkflowHistoryTool(server *mcp.Server, tempClient client.Client) error {
type GetWorkflowHistoryParams struct {
WorkflowID string `json:"workflowId"`
RunID string `json:"runId"`
}
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"
return server.RegisterTool("GetWorkflowHistory", desc, func(args GetWorkflowHistoryParams) (*mcp.ToolResponse, error) {
// Check if Temporal client is available
if tempClient == nil {
log.Printf("Error: Temporal client is not available for getting workflow histories")
return mcp.NewToolResponse(mcp.NewTextContent(
"Error: Temporal client is not available for getting workflow histories",
)), nil
}
eventJsons := make([]string, 0)
iterator := tempClient.GetWorkflowHistory(context.Background(), args.WorkflowID, args.RunID, false, temporal_enums.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT)
for iterator.HasNext() {
event, err := iterator.Next()
if err != nil {
msg := fmt.Sprintf("Error: Failed to get %dth history event: %v", len(eventJsons), err)
log.Print(msg)
return mcp.NewToolResponse(mcp.NewTextContent(msg)), nil
}
sanitize_history_event.SanitizeHistoryEvent(event)
bytes, err := protojson.Marshal(event)
if err != nil {
// should never happen?
return nil, err
}
eventJsons = append(eventJsons, string(bytes))
}
// The last step of json-marshalling is unfortunate (forced on us by the lack of a proto for the list of
// events), but not worth actually building and marshalling a slice for. Let's just do it by hand.
allEvents := strings.Builder{}
allEvents.WriteString("[")
for i, eventJson := range eventJsons {
if i > 0 {
allEvents.WriteString(",")
}
allEvents.WriteString(eventJson)
}
allEvents.WriteString("]")
return mcp.NewToolResponse(mcp.NewTextContent(allEvents.String())), nil
})
}
// registerSystemPrompt registers the system prompt for the MCP
func registerSystemPrompt(server *mcp.Server, cfg *config.Config) error {
return server.RegisterPrompt("system_prompt", "System prompt for the Temporal MCP", func(_ struct{}) (*mcp.PromptResponse, error) {
// Build list of available tools from workflows
workflowList := ""
for name, workflow := range cfg.Workflows {
// Use the complete purpose which already includes parameter details from config.yml
detailedPurpose := workflow.Purpose
workflowList += fmt.Sprintf("## %s\n", name)
workflowList += fmt.Sprintf("**Purpose:** %s\n\n", detailedPurpose)
workflowList += fmt.Sprintf("**Input Type:** %s\n\n", workflow.Input.Type)
// Add parameters section with detailed formatting based on the Input.Fields
workflowList += "**Parameters:**\n"
for _, field := range workflow.Input.Fields {
for fieldName, description := range field {
isRequired := !strings.Contains(description, "Optional")
if isRequired {
workflowList += fmt.Sprintf("- `%s` (required): %s\n", fieldName, description)
} else {
workflowList += fmt.Sprintf("- `%s` (optional): %s\n", fieldName, description)
}
}
}
// Add example of how to call this workflow
workflowList += "\n**Example Usage:**\n"
workflowList += "```json\n"
workflowList += "{\n \"params\": {\n"
// Generate example parameters
paramExamples := []string{}
for _, field := range workflow.Input.Fields {
for fieldName, _ := range field {
if strings.Contains(fieldName, "json") {
paramExamples = append(paramExamples, fmt.Sprintf(" \"%s\": {\"example\": \"value\"}", fieldName))
} else if strings.Contains(fieldName, "id") {
paramExamples = append(paramExamples, fmt.Sprintf(" \"%s\": \"example-id-123\"", fieldName))
} else {
paramExamples = append(paramExamples, fmt.Sprintf(" \"%s\": \"example value\"", fieldName))
}
}
}
workflowList += strings.Join(paramExamples, ",\n")
workflowList += "\n },\n \"force_rerun\": false\n}\n```\n"
// Add output information
workflowList += fmt.Sprintf("\n**Output Type:** %s\n", workflow.Output.Type)
if workflow.Output.Description != "" {
workflowList += fmt.Sprintf("**Output Description:** %s\n", workflow.Output.Description)
}
// Extract required parameters for validation guidance
var requiredParams []string
for _, field := range workflow.Input.Fields {
for fieldName, description := range field {
if !strings.Contains(description, "Optional") {
requiredParams = append(requiredParams, fieldName)
}
}
}
// Add validation guidelines
if len(requiredParams) > 0 {
workflowList += "\n**Required Validation:**\n"
workflowList += "- Validate all required parameters are provided before execution\n"
paramsList := strings.Join(requiredParams, ", ")
workflowList += fmt.Sprintf("- Required parameters: %s\n", paramsList)
}
workflowList += "\n---\n\n"
}
systemPrompt := fmt.Sprintf(`You are now connected to a Temporal MCP (Model Control Protocol) server that provides access to various Temporal workflows.
This MCP exposes the following workflow tools:
%s
## Parameter Validation Guidelines
Before executing any workflow, ensure you:
1. Validate all required parameters are present and properly formatted
2. Check that string parameters have appropriate length and format
3. Verify numeric parameters are within expected ranges
4. Ensure any IDs follow the proper format guidelines
5. Ask the user for any missing required parameters before execution
## Tool Usage Instructions
Use these tools to help users interact with Temporal workflows. Each workflow requires a 'params' object containing the necessary parameters listed above.
When constructing your calls:
- Include all required parameters
- Set force_rerun to true only when explicitly requested by the user
- When force_rerun is false, Temporal will deduplicate workflows based on their arguments
## General Example Structure
To call any workflow:
`+"```"+`
{
"params": {
"param1": "value1",
"param2": "value2"
},
"force_rerun": false
}
`+"```"+`
Refer to each workflow's specific example above for exact parameter requirements.`, workflowList)
return mcp.NewPromptResponse("system_prompt", mcp.NewPromptMessage(mcp.NewTextContent(systemPrompt), mcp.Role("system"))), nil
})
}
```