Sitemap
Sanjay Ramadugu

Sanjay Ramadugu

DevOps Engineer | Cloud Enthusiast | 5x Azure Certified | 2x Google Cloud

Building a Model Context Protocol Server for Hasura: Bringing Natural Language to GraphQL Management

8 min readNov 24, 2025

--

How I built an MCP server that lets you manage Hasura GraphQL Engine using natural language through AI assistants like Claude Desktop

Press enter or click to view image in full size

The Problem: GraphQL Management Complexity

If you’ve worked with Hasura GraphQL Engine, you know it’s powerful. You can spin up a production-ready GraphQL API in minutes, complete with real-time subscriptions, permissions, and relationships. But as your schema grows, managing metadata, debugging inconsistencies, and performing routine operations becomes increasingly complex.

I found myself repeatedly:

  • Exporting metadata to check configurations
  • Running SQL queries to inspect schemas
  • Debugging relationship issues
  • Setting up permissions across multiple tables
  • Checking for metadata inconsistencies after migrations

Each of these tasks required switching between the Hasura Console, CLI commands, and sometimes direct database queries. What if I could just ask for what I needed?

Enter: Model Context Protocol

The Model Context Protocol (MCP), developed by Anthropic, provides a standardized way for AI assistants to interact with external systems. Instead of building custom integrations for each tool, MCP creates a universal interface that any MCP-compliant client can use.

When I learned about MCP, I immediately saw the potential: What if I could talk to my Hasura instance through Claude Desktop using natural language?

What I Built

I created an MCP server that bridges Hasura GraphQL Engine with AI assistants. Now I can:

"Show me all tables in my database"
"Export the metadata for the users table"
"Check for any metadata inconsistencies"
"Help me set up permissions for the orders table"

Instead of remembering CLI commands or navigating the Console, I just ask.

Architecture Overview

The server is built as an extension to the Hasura CLI, leveraging its internal libraries for API communication. Here’s the high-level architecture:

┌─────────────────┐
│ Claude Desktop │
│ (MCP Client) │
└────────┬────────┘
│ stdio
│
┌────────▼────────┐
│ MCP Server │
│ (Go Binary) │
└────────┬────────┘
│ HTTP/GraphQL
│
┌────────▼────────┐
│ Hasura GraphQL │
│ Engine │
└─────────────────┘

Why Go?

I chose to build this in Go for several reasons:

  1. Native Hasura Integration: The Hasura CLI is written in Go, so I could leverage existing internal libraries
  2. Performance: Go’s compiled nature and concurrency model make it perfect for a server handling stdio communication
  3. Single Binary: Easy distribution — just one executable with no runtime dependencies
  4. MCP-Go SDK: The excellent mcp-go library by Mark3 Labs provided solid protocol implementation

Implementation Deep Dive

The Core Structure

The server implements three main MCP concepts:

1. Tools — Execute operations on Hasura:

// Example: Export Metadata Tool
func (h *MetadataHandler) ExportMetadata(ctx context.Context, params map[string]interface{}) (*mcp.ToolResponse, error) {
metadata, err := h.client.ExportMetadata()
if err != nil {
return nil, fmt.Errorf("failed to export metadata: %w", err)
}
    return &mcp.ToolResponse{
Content: []mcp.Content{{
Type: "text",
Text: marshalJSON(metadata),
}},
}, nil
}

2. Resources — Provide read-only access to data:

// Resource URIs like hasura://metadata, hasura://schema
func (h *ResourceHandler) GetResource(ctx context.Context, uri string) (*mcp.Resource, error) {
switch uri {
case "hasura://metadata":
return h.getMetadataResource(ctx)
case "hasura://version":
return h.getVersionResource(ctx)
// ... more resources
}
}

3. Prompts — Interactive workflows:

// Example: Table Setup Wizard
func (h *PromptHandler) SetupTable(ctx context.Context, args map[string]string) (*mcp.Prompt, error) {
return &mcp.Prompt{
Messages: []mcp.PromptMessage{
{
Role: "user",
Content: []mcp.Content{{
Type: "text",
Text: h.buildTableSetupPrompt(args),
}},
},
},
}, nil
}

Key Design Decisions

1. Read-Only Mode

Production safety was paramount. I added a --read-only flag that disables destructive operations:

if h.config.ReadOnly && isDestructiveOperation(toolName) {
return nil, fmt.Errorf("operation %s is not allowed in read-only mode", toolName)
}

Now I can safely connect to production instances without fear of accidental modifications.

2. Leveraging Hasura’s Internal Libraries

Rather than reimplementing API calls, I used Hasura’s own internal packages:

import (
"github.com/hasura/graphql-engine/cli/v2/internal/hasura"
"github.com/hasura/graphql-engine/cli/v2/internal/hasura/v1metadata"
"github.com/hasura/graphql-engine/cli/v2/internal/hasura/v2query"
)

This ensures compatibility and reduces maintenance burden. When Hasura updates their APIs, I benefit from their internal client updates.

3. Docker-First Distribution

The dependency on Hasura’s internal packages meant the code couldn’t be easily extracted. Solution? Docker.

# Multi-stage build for minimal image size
FROM golang:1.24-alpine AS builder
WORKDIR /build
COPY . ./
RUN go build -ldflags="-w -s" -o hasura-mcp-server ./cmd/mcp-server/main.go
FROM alpine:3.19
COPY --from=builder /build/hasura-mcp-server /app/hasura-mcp-server
USER hasura
ENTRYPOINT ["/app/hasura-mcp-server"]

Result: 41MB Alpine-based image that runs anywhere Docker does.

Setting Up Claude Desktop

Integration with Claude Desktop is straightforward. Add this to your config:

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

{
"mcpServers": {
"hasura": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"--network",
"host",
"sanjay3290/hasura-mcp:latest",
"--endpoint",
"http://localhost:8080",
"--admin-secret",
"your-admin-secret"
]
}
}
}

Restart Claude Desktop, and you’re ready to go.

Real-World Use Cases

1. Onboarding New Team Members

Me: "Show me all the tables in the system and explain their relationships"
Claude: *Uses get_schema tool to fetch schema, then explains the data model*

New developers get instant understanding without digging through documentation.

2. Debugging Metadata Issues

Me: "Check for metadata inconsistencies and explain what's wrong"
Claude: *Runs get_inconsistent_metadata, identifies issues, explains the problems*

Instead of parsing cryptic error messages, I get plain English explanations.

3. Quick Schema Queries

Me: "Count users created in the last 7 days"
Claude: *Executes SQL query via run_sql tool, returns results*

No need to open pgAdmin or write SQL manually.

4. Permission Configuration

Me: "Help me set up select permissions for the user role on the orders table"
Claude: *Uses setup_permissions prompt to guide through configuration*

Interactive setup with validation and best practices.

Capabilities Overview

The server provides 17 total capabilities:

Tools (9 operations)

  • export_metadata - Export Hasura metadata
  • apply_metadata - Apply metadata changes
  • reload_metadata - Reload from database
  • clear_metadata - Clear all metadata
  • get_inconsistent_metadata - List inconsistencies
  • drop_inconsistent_metadata - Drop bad objects
  • run_sql - Execute SQL queries
  • get_version - Version information
  • get_schema - Database schema

Resources (4 read-only endpoints)

  • hasura://metadata - Current metadata
  • hasura://version - Version details
  • hasura://schema - Schema information
  • hasura://inconsistent-metadata - Issues list

Prompts (4 interactive workflows)

  • setup_table - Table configuration wizard
  • debug_metadata - Debugging assistant
  • create_migration - Migration helper
  • setup_permissions - Permission setup guide

Technical Challenges & Solutions

Challenge 1: Stdio Communication

MCP uses stdio for client-server communication. This required careful handling of buffered I/O:

// Proper stdio handling
scanner := bufio.NewScanner(os.Stdin)
writer := bufio.NewWriter(os.Stdout)
for scanner.Scan() {
request := scanner.Bytes()
response, err := h.handleRequest(request)
if err != nil {
logrus.WithError(err).Error("Request handling failed")
continue
}
writer.Write(response)
writer.Flush() // Critical for MCP protocol
}

Challenge 2: JSON-RPC 2.0 Error Handling

MCP uses JSON-RPC 2.0, which has specific error format requirements:

type ErrorResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// Map Go errors to JSON-RPC error codes
func mapError(err error) ErrorResponse {
switch {
case errors.Is(err, ErrUnauthorized):
return ErrorResponse{Code: -32001, Message: "Unauthorized"}
case errors.Is(err, ErrInvalidParams):
return ErrorResponse{Code: -32602, Message: "Invalid params"}
default:
return ErrorResponse{Code: -32603, Message: err.Error()}
}
}

Challenge 3: Dependency Management

The Hasura CLI has many dependencies. I had to ensure proper module management:

// go.mod
require (
github.com/hasura/graphql-engine/cli/v2 v2.48.7
github.com/mark3labs/mcp-go v0.1.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/pflag v1.0.5
)

Building inside the Hasura repository context ensures all dependencies resolve correctly.

Distribution Strategy

Source Code

Repository: https://github.com/sanjay3290/graphql-engine Location: cli/cmd/mcp-server/

Get Sanjay Ramadugu’s stories in your inbox

Join Medium for free to get updates from this writer.

The source code lives in a fork of the Hasura repository to maintain the full build context.

Docker Image

Registry: Docker Hub at sanjay3290/hasura-mcp Tags: latest, 1.0.0

Docker provides universal distribution without worrying about Go installations or dependencies.

Documentation

Repository: https://github.com/sanjay3290/hasura-mcp-server

A dedicated documentation repository provides:

  • Quick start guides
  • Usage examples
  • Troubleshooting tips
  • Contributing guidelines

This separation keeps the documentation accessible without cloning the massive Hasura repository.

Performance Characteristics

The server is lightweight and efficient:

  • Image Size: 41MB (Alpine-based)
  • Memory Usage: ~20MB runtime
  • Startup Time: <100ms
  • Request Latency: Dominated by Hasura API calls, not the MCP layer

Benchmarks

Testing against a local Hasura instance:

Operation              Time (avg)
-------------------------------------
export_metadata 145ms
get_schema 89ms
run_sql (simple) 52ms
get_version 38ms

The MCP protocol overhead is negligible (<5ms per request).

Security Considerations

Admin Secret Handling

Never hardcode secrets. Use environment variables or secure configuration:

{
"mcpServers": {
"hasura": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "HASURA_GRAPHQL_ENDPOINT=http://localhost:8080",
"-e", "HASURA_GRAPHQL_ADMIN_SECRET=${HASURA_ADMIN_SECRET}",
"sanjay3290/hasura-mcp:latest"
]
}
}
}

Network Isolation

The Docker container runs with minimal privileges:

# Non-root user
RUN addgroup -g 1000 hasura && \
adduser -D -u 1000 -G hasura hasura
USER hasura
# No exposed ports (stdio communication only)

Read-Only Mode for Production

Always use --read-only for production instances:

docker run -i --rm \
sanjay3290/hasura-mcp:latest \
--endpoint https://prod-hasura.com \
--admin-secret $PROD_SECRET \
--read-only

Lessons Learned

1. Start with Use Cases

I began by listing my daily Hasura tasks, then built tools to support them. This kept the scope focused and practical.

2. Leverage Existing Code

By using Hasura’s internal libraries, I avoided reimplementing complex logic and ensured compatibility.

3. Documentation Matters

A tool is only useful if people can use it. I invested heavily in documentation, examples, and troubleshooting guides.

4. Docker Simplifies Distribution

Docker eliminated “works on my machine” issues. Users just run a container — no Go installation, no dependency management.

5. Read-Only Mode is Essential

Production safety through read-only mode gave me confidence to connect to live systems.

Future Enhancements

I’m considering several additions:

1. Batch Operations

"Export metadata for all tables in the public schema"

2. Metadata Diffing

"Compare current metadata with the version from last week"

3. Migration Preview

"Show me what SQL will be generated for this metadata change"

4. GraphQL Query Execution

Direct GraphQL queries, not just SQL:

"Run this GraphQL query: { users { id name } }"

5. Relationship Visualization

"Show me a diagram of table relationships"

Performance Optimizations

Response Caching

For expensive operations like schema queries:

type CachedResponse struct {
Data interface{}
Timestamp time.Time
TTL time.Duration
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
if resp, ok := c.data[key]; ok {
if time.Since(resp.Timestamp) < resp.TTL {
return resp.Data, true
}
}
return nil, false
}

Connection Pooling

Reuse HTTP clients for better performance:

var defaultClient = &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
},
}

Community and Open Source

This project is fully open source under Apache 2.0 license. I encourage:

  • Bug reports: Found an issue? Open a ticket
  • Feature requests: Have an idea? Let’s discuss it
  • Pull requests: Contributions welcome
  • Documentation: Help improve the guides

Attribution

This project builds on amazing open source work:

  • Hasura GraphQL Engine — The foundation
  • MCP-Go SDK by Mark3 Labs — Protocol implementation
  • Model Context Protocol by Anthropic — The standard

Getting Started

Ready to try it? Here’s the 5-minute setup:

1. Pull the Image

docker pull sanjay3290/hasura-mcp:latest

2. Configure Claude Desktop

Add to ~/Library/Application Support/Claude/claude_desktop_config.json:

{
"mcpServers": {
"hasura": {
"command": "docker",
"args": [
"run", "-i", "--rm", "--network", "host",
"sanjay3290/hasura-mcp:latest",
"--endpoint", "http://localhost:8080",
"--admin-secret", "your-secret"
]
}
}
}

3. Restart Claude Desktop

Completely quit and reopen.

4. Try It Out

"What's my Hasura version?"
"Show me all tables"
"Export the metadata"

Technical Resources

Conclusion

Building this MCP server transformed how I interact with Hasura. What used to require remembering CLI commands or navigating the Console now happens through natural conversation.

The combination of:

  • MCP protocol for standardized communication
  • Go for performance and single-binary distribution
  • Docker for universal deployment
  • Hasura’s internal libraries for reliable API access

…created a tool that’s both powerful and easy to use.

If you work with Hasura GraphQL Engine, I encourage you to try it. And if you have ideas for improvements, the project is open source — contributions welcome!

About Me

I’m Sanjay Ramadugu, a platform engineer focused on developer experience and infrastructure automation. I work with Hasura extensively and built this tool to solve my own pain points. Find me on GitHub at @sanjay3290.

--

Sanjay Ramadugu
Sanjay Ramadugu

Written by Sanjay Ramadugu

DevOps Engineer | Cloud Enthusiast | 5x Azure Certified | 2x Google Cloud