Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pkg/server/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (

clickhouseplugin "github.com/ethpandaops/mcp/plugins/clickhouse"
doraplugin "github.com/ethpandaops/mcp/plugins/dora"
labplugin "github.com/ethpandaops/mcp/plugins/lab"
lokiplugin "github.com/ethpandaops/mcp/plugins/loki"
prometheusplugin "github.com/ethpandaops/mcp/plugins/prometheus"
)
Expand Down Expand Up @@ -201,6 +202,7 @@ func (b *Builder) buildPluginRegistry() (*plugin.Registry, error) {
// Register all compiled-in plugins.
reg.Add(clickhouseplugin.New())
reg.Add(doraplugin.New())
reg.Add(labplugin.New())
reg.Add(lokiplugin.New())
reg.Add(prometheusplugin.New())

Expand Down
47 changes: 47 additions & 0 deletions plugins/lab/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package lab

// Config holds the Lab plugin configuration.
// Lab is enabled by default since it's a public service and
// requires no credentials.
type Config struct {
// Enabled controls whether the Lab plugin is active.
// Defaults to true.
Enabled *bool `yaml:"enabled,omitempty"`

// RoutesURL is the URL to fetch the lab routes.json from.
// Defaults to https://raw.githubusercontent.com/ethpandaops/lab/main/routes.json
RoutesURL string `yaml:"routes_url,omitempty"`

// SkillURL is the URL to fetch the lab SKILL.md from.
// Defaults to https://raw.githubusercontent.com/ethpandaops/lab/main/SKILL.md
SkillURL string `yaml:"skill_url,omitempty"`

// Networks allows specifying custom Lab URLs for networks.
// If not specified, default URLs will be used for known networks.
Networks map[string]string `yaml:"networks,omitempty"`
}

// IsEnabled returns true if the plugin is enabled (default: true).
func (c *Config) IsEnabled() bool {
if c.Enabled == nil {
return true
}

return *c.Enabled
}

// GetRoutesURL returns the routes.json URL with default.
func (c *Config) GetRoutesURL() string {
if c.RoutesURL == "" {
return "https://raw.githubusercontent.com/ethpandaops/lab/main/routes.json"
}
return c.RoutesURL
}

// GetSkillURL returns the SKILL.md URL with default.
func (c *Config) GetSkillURL() string {
if c.SkillURL == "" {
return "https://raw.githubusercontent.com/ethpandaops/lab/main/SKILL.md"
}
return c.SkillURL
}
29 changes: 29 additions & 0 deletions plugins/lab/examples.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package lab

import (
_ "embed"
"fmt"
"strings"

"github.com/ethpandaops/mcp/pkg/types"
"gopkg.in/yaml.v3"
)

//go:embed examples.yaml
var examplesYAML []byte

var queryExamples map[string]types.ExampleCategory

func init() {
if err := yaml.Unmarshal(examplesYAML, &queryExamples); err != nil {
panic(fmt.Sprintf("failed to parse lab examples.yaml: %v", err))
}

for key, category := range queryExamples {
for i := range category.Examples {
category.Examples[i].Query = strings.TrimSpace(category.Examples[i].Query)
}

queryExamples[key] = category
}
}
187 changes: 187 additions & 0 deletions plugins/lab/examples.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
lab_network_discovery:
name: Network Discovery
description: Discover Lab-enabled networks and available routes
examples:
- name: List Lab networks
description: Get all networks with Lab explorers
query: |
from ethpandaops import lab

# List available networks with Lab explorers
networks = lab.list_networks()
print(f"Available networks: {[n['name'] for n in networks]}")

# Get base URL for a specific network
if networks:
base_url = lab.get_base_url(networks[0]['name'])
print(f"Base URL: {base_url}")

- name: Get all routes
description: Fetch and display all Lab routes from routes.json
query: |
from ethpandaops import lab

# Get all routes organized by category
routes = lab.get_routes()

for category, route_list in routes.items():
print(f"\n{category}:")
for route in route_list:
print(f" - {route['id']}: {route.get('description', 'No description')}")

- name: Get routes by category
description: Filter routes to a specific category
query: |
from ethpandaops import lab

# Get only Ethereum routes
eth_routes = lab.get_routes_by_category("ethereum")
print(f"Ethereum routes ({len(eth_routes)}):")
for route in eth_routes:
print(f" - {route['id']}: {route.get('path', 'N/A')}")

lab_deep_links:
name: Deep Links
description: Generate Lab explorer links for various Ethereum entities
examples:
- name: Generate slot and epoch links
description: Create deep links to slots and epochs
query: |
from ethpandaops import lab

network = "mainnet"
slot = 1000000
epoch = 31250

# Slot link
slot_link = lab.link_slot(network, slot)
print(f"Slot {slot}: {slot_link}")

# Epoch link
epoch_link = lab.link_epoch(network, epoch)
print(f"Epoch {epoch}: {epoch_link}")

- name: Generate validator and address links
description: Create deep links to validators and addresses
query: |
from ethpandaops import lab

network = "sepolia"

# Validator link
validator_link = lab.link_validator(network, "12345")
print(f"Validator: {validator_link}")

# Address link
address_link = lab.link_address(network, "0x742d35Cc6634C0532925a3b844Bc9e7595f1c1")
print(f"Address: {address_link}")

- name: Generate block and transaction links
description: Create deep links to blocks and transactions
query: |
from ethpandaops import lab

network = "holesky"

# Block link
block_link = lab.link_block(network, "1000000")
print(f"Block: {block_link}")

# Transaction link
tx_link = lab.link_transaction(network, "0xabc123...")
print(f"Transaction: {tx_link}")

- name: Generate blob and fork links
description: Create deep links to blobs and fork information
query: |
from ethpandaops import lab

network = "mainnet"

# Blob link
blob_link = lab.link_blob(network, "0x1234...")
print(f"Blob: {blob_link}")

# Fork link
fork_link = lab.link_fork(network, "cancun")
print(f"Fork: {fork_link}")

lab_custom_urls:
name: Custom URL Building
description: Build custom URLs using route patterns and parameters
examples:
- name: Build URLs with parameters
description: Use build_url for flexible URL generation
query: |
from ethpandaops import lab

# Build a URL for a specific route
url = lab.build_url("mainnet", "slot", {"slot": 1000000})
print(f"Custom URL: {url}")

# Get route metadata first
route = lab.get_route("ethereum/slots")
if route:
print(f"Route path: {route.get('path')}")
print(f"Parameters: {route.get('parameters', [])}")

- name: Multi-network link generation
description: Generate links across multiple networks
query: |
from ethpandaops import lab

networks = ["mainnet", "sepolia", "holesky", "hoodi"]
slot = 1000000

print(f"Slot {slot} across networks:")
for network in networks:
try:
link = lab.link_slot(network, slot)
print(f" {network}: {link}")
except ValueError as e:
print(f" {network}: Not available ({e})")

lab_combined_workflows:
name: Combined Workflows
description: Combine Lab with other data sources for comprehensive analysis
examples:
- name: Network analysis with Lab links
description: Combine Lab with ClickHouse data
query: |
from ethpandaops import lab, clickhouse

network = "holesky"

# Get recent blocks from ClickHouse
df = clickhouse.query("xatu", f'''
SELECT
slot,
block_root,
proposer_index
FROM beacon_api_eth_v1_events_block
WHERE meta_network_name = '{network}'
ORDER BY slot DESC
LIMIT 5
''')

print(f"Recent blocks on {network}:")
for _, row in df.iterrows():
link = lab.link_slot(network, row['slot'])
print(f" Slot {row['slot']}: proposer {row['proposer_index']}")
print(f" View: {link}")

- name: Cross-explorer comparison
description: Compare Lab and Dora links for the same entity
query: |
from ethpandaops import lab, dora

network = "holesky"
slot = 1000000

# Get links from both explorers
lab_link = lab.link_slot(network, slot)
dora_link = dora.link_slot(network, str(slot))

print(f"Slot {slot} explorer links:")
print(f" Lab: {lab_link}")
print(f" Dora: {dora_link}")
Loading