#
tokens: 6018/50000 9/9 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── cmd
│   └── server
│       └── main.go
├── go.mod
├── go.sum
├── internal
│   ├── app
│   │   └── server.go
│   ├── handlers
│   │   ├── contact.go
│   │   ├── http.go
│   │   └── signature.go
│   └── tools
│       ├── contact.go
│       └── signature.go
├── LICENSE
└── 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
*.out

# Dependency directories
vendor/

# Go workspace file
go.work

# IDE specific files
.idea/
.vscode/
*.swp
*.swo
.DS_Store

# Environment files
.env
.env.local
.env.*.local

# Log files
*.log

# Binary output directory
bin/
dist/
```

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

```markdown
# Signaturit MCP ✍️

> **Note:** This is an unofficial integration project and is not affiliated with, officially maintained, or endorsed by Signaturit.

This project is a demonstration of an MCP (Microservice Communication Protocol) server that integrates with Signaturit tools through their [public API](https://www.signaturit.com/api). It provides various functionalities for managing signature requests, including listing, creating, and handling signatures.

## Capabilities 🚀

The MCP server provides the following tools to interact with Signaturit:

- **get_signature** 📄: Retrieve details of a specific signature request using its ID
- **create_signature** ✨: Create new signature requests using templates
  - Support for multiple signers 👥
  - Email or SMS delivery 📧 📱
  - Customizable expiration time ⏰
  - Sequential or parallel signing workflow ⛓️
  - Custom email/SMS messages 💬
  - Webhook integration for real-time notifications 🔔
- **send_signature_reminder** 📬: Send reminder notifications to pending signers
- **cancel_signature** ❌: Cancel active signature requests with optional reason

## Project Structure 📁

- **cmd/server/main.go** 🎯: Entry point of the application. It initializes and starts the MCP server.
- **internal/app/server.go** ⚙️: Contains the logic for creating and configuring the MCP server, including registering signature tools and handlers.
- **internal/handlers/signature.go** 🛠️: Implements handler functions for various signature operations such as listing, retrieving, and managing signatures.
- **internal/tools/signature.go** 🔧: Registers signature-related tools with the MCP server.

## Configuration ⚙️

### API Authentication 🔐

This server integrates with the Signaturit API and requires an API key for authentication. You need to:

1. Create an account at [Signaturit](https://www.signaturit.com)
2. Get your API key from the Signaturit dashboard
3. Set the API key as an environment variable:

```bash
export SIGNATURIT_SECRET_TOKEN='your_api_key_here'
```

## Prerequisites 📋

1. **Go Installation** 
   - Go 1.16 or higher
   - Verify your installation:
   ```bash
   go version
   ```

2. **Signaturit Account** 
   - Active account at [Signaturit](https://www.signaturit.com)
   - Valid API key from the Signaturit dashboard

## Build 🔨

1. **Clone the repository**
   ```bash
   git clone https://github.com/yourusername/signaturtit_mcp.git
   cd signaturtit_mcp
   ```

2. **Install dependencies**
   ```bash
   go mod download
   ```

3. **Build the application**
   ```bash
   # Build for your current platform
   go build -o bin/signaturtit_mcp cmd/server/main.go

   # Build for specific platform (e.g., Linux)
   GOOS=linux GOARCH=amd64 go build -o bin/signaturtit_mcp cmd/server/main.go
   ```

4. **Run the built binary**
   ```bash
   # Make sure you have set the required environment variables first
   export SIGNATURIT_SECRET_TOKEN='your_api_key_here'
   
   # Run the application
   ./bin/signaturtit_mcp
   ```

## License 📜

```
Copyright 2024 Jordi Martin

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```

```

--------------------------------------------------------------------------------
/cmd/server/main.go:
--------------------------------------------------------------------------------

```go
package main

import (
	"fmt"
	"log"

	"github.com/mark3labs/mcp-go/server"
	"signaturit.com/mcp/internal/app"
)

func main() {
	// Create the MCP server using our application logic.
	s := app.NewMCPServer()

	// Start the MCP server on stdio.
	if err := server.ServeStdio(s); err != nil {
		log.Fatalf("Server error: %v\n", err)
	} else {
		fmt.Println("MCP Server stopped gracefully.")
	}
}

```

--------------------------------------------------------------------------------
/internal/app/server.go:
--------------------------------------------------------------------------------

```go
package app

import (
	"os"

	"github.com/mark3labs/mcp-go/server"
	"signaturit.com/mcp/internal/handlers"
	"signaturit.com/mcp/internal/tools"
)

// NewMCPServer creates and configures the MCP server.
func NewMCPServer() *server.MCPServer {
	// Create a new MCP server.
	s := server.NewMCPServer(
		"Signaturit Tools Demo",
		"1.0.0",
		server.WithLogging(),
	)

	// Get api key from environment variable
	apikey := os.Getenv("SIGNATURIT_SECRET_TOKEN")
	// Create a new handler for the signature tools.
	// This handler will be used to process the requests for the signature tools.
	h := handlers.NewHandler(apikey, false)

	// Register the signature tools.
	tools.InitSignatureTools(s, h)
	tools.InitContactTools(s, h)

	// Register the handlers for each tool.
	// The references to the handlers are set in InitSignatureTools.

	return s
}

```

--------------------------------------------------------------------------------
/internal/tools/contact.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"signaturit.com/mcp/internal/handlers"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

// InitContactTools registers all contact-related MCP tools.
func InitContactTools(s *server.MCPServer, handler *handlers.Handler) {
	// List Contacts Tool
	listContactsTool := mcp.NewTool(
		"list_contacts",
		mcp.WithDescription("Get all contacts from your Signaturit account"),
	)
	s.AddTool(listContactsTool, handler.ListContacts)

	// Get Contact Tool
	getContactTool := mcp.NewTool(
		"get_contact",
		mcp.WithDescription("Get a single contact by ID"),
		mcp.WithString("contact_id",
			mcp.Required(),
			mcp.Description("ID of the contact to retrieve (e.g., e8125099-871e-11e6-88d5-06875124f8dd)"),
		),
	)
	s.AddTool(getContactTool, handler.GetContact)

	// Create Contact Tool
	createContactTool := mcp.NewTool(
		"create_contact",
		mcp.WithDescription("Create a new contact in your Signaturit account"),
		mcp.WithString("email",
			mcp.Required(),
			mcp.Description("Email of the new contact (e.g., [email protected])"),
		),
		mcp.WithString("name",
			mcp.Required(),
			mcp.Description("Name of the new contact (e.g., John Doe)"),
		),
	)
	s.AddTool(createContactTool, handler.CreateContact)

	// Update Contact Tool
	updateContactTool := mcp.NewTool(
		"update_contact",
		mcp.WithDescription("Update an existing contact's information"),
		mcp.WithString("contact_id",
			mcp.Required(),
			mcp.Description("ID of the contact to update"),
		),
		mcp.WithString("email",
			mcp.Description("New email for the contact (optional)"),
		),
		mcp.WithString("name",
			mcp.Description("New name for the contact (optional)"),
		),
	)
	s.AddTool(updateContactTool, handler.UpdateContact)

	// Delete Contact Tool
	deleteContactTool := mcp.NewTool(
		"delete_contact",
		mcp.WithDescription("Delete a contact from your Signaturit account"),
		mcp.WithString("contact_id",
			mcp.Required(),
			mcp.Description("ID of the contact to delete"),
		),
	)
	s.AddTool(deleteContactTool, handler.DeleteContact)
}

```

--------------------------------------------------------------------------------
/internal/handlers/http.go:
--------------------------------------------------------------------------------

```go
package handlers

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

const (
	sandboxURL    = "https://api.sandbox.signaturit.com/v3"
	productionURL = "https://api.signaturit.com/v3"
)

// SignaturitClient represents the HTTP client for Signaturit API
type SignaturitClient struct {
	client  *http.Client
	apiKey  string
	baseURL string
	debug   bool
}

// NewSignaturitClient creates a new Signaturit API client
func NewSignaturitClient(apiKey string, debug bool) *SignaturitClient {
	baseURL := productionURL
	if debug {
		baseURL = sandboxURL
	}

	return &SignaturitClient{
		client:  &http.Client{},
		apiKey:  apiKey,
		baseURL: baseURL,
		debug:   debug,
	}
}

// doRequest performs the HTTP request with authentication header
func (c *SignaturitClient) doRequest(method, endpoint string, body interface{}) (*http.Response, error) {
	var bodyReader io.Reader

	if body != nil {
		jsonBody, err := json.Marshal(body)
		if err != nil {
			return nil, fmt.Errorf("error marshaling request body: %w", err)
		}
		bodyReader = bytes.NewBuffer(jsonBody)
	}

	url := fmt.Sprintf("%s%s", c.baseURL, endpoint)
	req, err := http.NewRequest(method, url, bodyReader)
	if err != nil {
		return nil, fmt.Errorf("error creating request: %w", err)
	}

	// Add authentication header
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
	req.Header.Set("Content-Type", "application/json")

	resp, err := c.client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("error executing request: %w", err)
	}

	return resp, nil
}

// Get performs a GET request
func (c *SignaturitClient) Get(endpoint string) (*http.Response, error) {
	return c.doRequest(http.MethodGet, endpoint, nil)
}

// Post performs a POST request
func (c *SignaturitClient) Post(endpoint string, body interface{}) (*http.Response, error) {
	return c.doRequest(http.MethodPost, endpoint, body)
}

// Patch performs a PATCH request
func (c *SignaturitClient) Patch(endpoint string, body interface{}) (*http.Response, error) {
	return c.doRequest(http.MethodPatch, endpoint, body)
}

// Delete performs a DELETE request
func (c *SignaturitClient) Delete(endpoint string) (*http.Response, error) {
	return c.doRequest(http.MethodDelete, endpoint, nil)
}

```

--------------------------------------------------------------------------------
/internal/tools/signature.go:
--------------------------------------------------------------------------------

```go
package tools

import (
	"signaturit.com/mcp/internal/handlers"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/mark3labs/mcp-go/server"
)

// InitSignatureTools registers all signature-related MCP tools.
func InitSignatureTools(s *server.MCPServer, handler *handlers.Handler) {

	//Get Single Signature
	getSignatureTool := mcp.NewTool(
		"get_signature",
		mcp.WithDescription("Retrieve a single signature request by ID"),
		mcp.WithString("signature_id",
			mcp.Required(),
			mcp.Description("ID of the signature request to retrieve"),
		),
	)
	s.AddTool(getSignatureTool, handler.GetSignature)

	//Create Signature Request with Templates
	createSignatureTool := mcp.NewTool(
		"create_signature",
		mcp.WithDescription("Create a new signature request (multi-signer, with optional custom data) using templates instead of file uploads"),
		mcp.WithString("templates",
			mcp.Required(),
			mcp.Description("Comma-separated list of template IDs or hashtags to use for the signature request. For example: #NDA,abc123"),
		),
		mcp.WithString("recipients",
			mcp.Required(),
			mcp.Description("List of signer objects, each with name, email (or phone if SMS), and optional advanced settings"),
		),
		mcp.WithString("body",
			mcp.Description("Body message for the email or SMS (HTML allowed in email). OPTIONAL"),
		),
		mcp.WithString("subject",
			mcp.Description("Subject for the email request. OPTIONAL"),
		),
		mcp.WithString("type",
			mcp.Description("Delivery type: 'email' (default), 'sms', or 'wizard'. OPTIONAL"),
			mcp.DefaultString("email"),
		),
		mcp.WithNumber("expires_in_days",
			mcp.Description("Number of days before the signature request expires (1–365). OPTIONAL"),
		),
		mcp.WithString("event_url",
			mcp.Description("Callback URL for receiving real-time notifications. OPTIONAL"),
		),
		mcp.WithString("signing_mode",
			mcp.Description("Signing order: 'sequential' (default) or 'parallel'. OPTIONAL"),
		),
	)
	s.AddTool(createSignatureTool, handler.CreateSignature)

	//Send Reminder
	sendSignatureReminderTool := mcp.NewTool(
		"send_signature_reminder",
		mcp.WithDescription("Send a reminder email/SMS to the signer of a pending signature"),
		mcp.WithString("signature_id",
			mcp.Required(),
			mcp.Description("ID of the signature request to remind"),
		),
	)
	s.AddTool(sendSignatureReminderTool, handler.SendSignatureReminder)

	//Cancel Signature Request
	cancelSignatureTool := mcp.NewTool(
		"cancel_signature",
		mcp.WithDescription("Cancel an in-progress signature so it can no longer be signed"),
		mcp.WithString("signature_id",
			mcp.Required(),
			mcp.Description("ID of the signature request to cancel"),
		),
		mcp.WithString("reason",
			mcp.Description("Optional reason for canceling the signature request"),
		),
	)
	s.AddTool(cancelSignatureTool, handler.CancelSignature)
}

```

--------------------------------------------------------------------------------
/internal/handlers/contact.go:
--------------------------------------------------------------------------------

```go
package handlers

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"

	"github.com/mark3labs/mcp-go/mcp"
)

type Contact struct {
	ID        string `json:"id"`
	Email     string `json:"email"`
	Name      string `json:"name"`
	CreatedAt string `json:"created_at"`
}

func (h *Handler) ListContacts(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	resp, err := h.client.Get("/contacts.json")
	if err != nil {
		return nil, fmt.Errorf("failed to list contacts: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
	}

	var contacts []Contact
	if err := json.Unmarshal(body, &contacts); err != nil {
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	summary := "Contacts:\n"
	for _, contact := range contacts {
		summary += fmt.Sprintf("- %s (%s) [ID: %s]\n", contact.Name, contact.Email, contact.ID)
	}

	return mcp.NewToolResultText(summary), nil
}

func (h *Handler) GetContact(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	contactID := req.Params.Arguments["contact_id"].(string)

	resp, err := h.client.Get(fmt.Sprintf("/contacts/%s.json", contactID))
	if err != nil {
		return nil, fmt.Errorf("failed to get contact: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
	}

	var contact Contact
	if err := json.Unmarshal(body, &contact); err != nil {
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	return mcp.NewToolResultText(fmt.Sprintf("Contact: %s (%s) [ID: %s]", contact.Name, contact.Email, contact.ID)), nil
}

func (h *Handler) CreateContact(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	email := req.Params.Arguments["email"].(string)
	name := req.Params.Arguments["name"].(string)

	requestBody := map[string]interface{}{
		"email": email,
		"name":  name,
	}

	resp, err := h.client.Post("/contacts.json", requestBody)
	if err != nil {
		return nil, fmt.Errorf("failed to create contact: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
	}

	var contact Contact
	if err := json.Unmarshal(body, &contact); err != nil {
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	return mcp.NewToolResultText(fmt.Sprintf("Contact created: %s (%s) [ID: %s]", contact.Name, contact.Email, contact.ID)), nil
}

func (h *Handler) UpdateContact(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	contactID := req.Params.Arguments["contact_id"].(string)

	requestBody := make(map[string]interface{})
	if email, ok := req.Params.Arguments["email"].(string); ok && email != "" {
		requestBody["email"] = email
	}
	if name, ok := req.Params.Arguments["name"].(string); ok && name != "" {
		requestBody["name"] = name
	}

	resp, err := h.client.Patch(fmt.Sprintf("/contacts/%s.json", contactID), requestBody)
	if err != nil {
		return nil, fmt.Errorf("failed to update contact: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
	}

	var contact Contact
	if err := json.Unmarshal(body, &contact); err != nil {
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	return mcp.NewToolResultText(fmt.Sprintf("Contact updated: %s (%s) [ID: %s]", contact.Name, contact.Email, contact.ID)), nil
}

func (h *Handler) DeleteContact(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	contactID := req.Params.Arguments["contact_id"].(string)

	resp, err := h.client.Delete(fmt.Sprintf("/contacts/%s.json", contactID))
	if err != nil {
		return nil, fmt.Errorf("failed to delete contact: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
	}

	return mcp.NewToolResultText(fmt.Sprintf("Contact %s successfully deleted", contactID)), nil
}

```

--------------------------------------------------------------------------------
/internal/handlers/signature.go:
--------------------------------------------------------------------------------

```go
package handlers

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"

	"github.com/mark3labs/mcp-go/mcp"
)

type Recipient struct {
	Name  string `json:"name"`
	Email string `json:"email"`
}

type ResponseGetSignature struct {
	CreatedAt string     `json:"created_at"`
	Data      []any      `json:"data"`
	Documents []Document `json:"documents"`
}

type Document struct {
	Email  string  `json:"email"`
	Events []Event `json:"events"`
	File   File    `json:"file"`
	ID     string  `json:"id"` // Document-level ID
	Name   string  `json:"name"`
	Status string  `json:"status"`
}

type Event struct {
	CreatedAt string `json:"created_at"`
	Type      string `json:"type"`
}

type File struct {
	ID    string `json:"id"` // File-level ID
	Name  string `json:"name"`
	Pages int    `json:"pages"`
	Size  int    `json:"size"`
}

// Handler handles signature-related operations
type Handler struct {
	client *SignaturitClient
}

// NewHandler creates a new signature handler
func NewHandler(apiKey string, debug bool) *Handler {
	return &Handler{
		client: NewSignaturitClient(apiKey, debug),
	}
}

func (h *Handler) GetSignature(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	signatureID := req.Params.Arguments["signature_id"].(string)

	// Make API call
	resp, err := h.client.Get(fmt.Sprintf("/signatures/%s.json", signatureID))
	if err != nil {
		return nil, fmt.Errorf("failed to get signature: %w", err)
	}
	defer resp.Body.Close()

	// Read response
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	// Check response status
	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
	}

	// Parse response
	var response ResponseGetSignature
	if err := json.Unmarshal(body, &response); err != nil {
		return nil, fmt.Errorf("failed to parse response: %w", err)
	}

	ready := true
	summary := ""
	for _, document := range response.Documents {
		summary += fmt.Sprintf("Document %s: Send to %s is %s\n", document.ID, document.Email, document.Status)
		for _, event := range document.Events {
			summary += fmt.Sprintf("  - %s at %s\n", event.Type, event.CreatedAt)
		}
		if document.Status != "completed" {
			ready = false
		}
	}

	return mcp.NewToolResultText(fmt.Sprintf("Signature ID %s created at %s, summary:\n%s\nComplete:%s", signatureID, response.CreatedAt, summary, strconv.FormatBool(ready))), nil
}

func (h *Handler) CreateSignature(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	st := req.Params.Arguments["templates"].(string)
	templates := strings.Split(st, ",")
	sr := req.Params.Arguments["recipients"].(string)
	expiresInDays, errEx := req.Params.Arguments["expires_in_days"].(float64)
	if !errEx {
		expiresInDays = 7
	}

	var recipients []Recipient
	if err := json.Unmarshal([]byte(sr), &recipients); err != nil {
		return nil, fmt.Errorf("failed to parse recipients: %w", err)
	}

	// Prepare request body
	requestBody := map[string]interface{}{
		"templates":  templates,
		"recipients": recipients,
		"expires_in": expiresInDays,
		"body":       req.Params.Arguments["body"].(string),
		"subject":    req.Params.Arguments["subject"].(string),
	}

	// Make API call
	resp, err := h.client.Post("/signatures.json", requestBody)
	if err != nil {
		return nil, fmt.Errorf("failed to create signature: %w", err)
	}
	defer resp.Body.Close()

	// Read response
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	// Check response status
	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
	}

	return mcp.NewToolResultText(fmt.Sprintf("Templates %v was send to %v to sign with reponse: %s", templates, recipients, string(body))), nil
}

func (h *Handler) SendSignatureReminder(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	signatureID := req.Params.Arguments["signature_id"].(string)
	resp, err := h.client.Post(fmt.Sprintf("/signatures/%s/reminders.json", signatureID), nil)
	if err != nil {
		return nil, fmt.Errorf("failed to send reminder: %w", err)
	}
	defer resp.Body.Close()

	// Check response status
	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode)
	}

	return mcp.NewToolResultText(fmt.Sprintf("Sending reminder for signature %s...", signatureID)), nil
}

func (h *Handler) CancelSignature(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	signatureID := req.Params.Arguments["signature_id"].(string)
	reason, _ := req.Params.Arguments["reason"].(string)

	resp, err := h.client.Patch(fmt.Sprintf("/signatures/%s.json", signatureID), map[string]interface{}{"reason": reason})
	if err != nil {
		return nil, fmt.Errorf("failed to cancel signature: %w", err)
	}
	defer resp.Body.Close()

	// Check response status
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode)
	}

	return mcp.NewToolResultText(fmt.Sprintf("Canceling signature %s. Reason: %s", signatureID, reason)), nil
}

```