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 cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/daltoniam/switchboard/integrations/jira"
"github.com/daltoniam/switchboard/integrations/linear"
"github.com/daltoniam/switchboard/integrations/metabase"
"github.com/daltoniam/switchboard/integrations/mixpanel"
notionInt "github.com/daltoniam/switchboard/integrations/notion"
"github.com/daltoniam/switchboard/integrations/pganalyze"
"github.com/daltoniam/switchboard/integrations/postgres"
Expand Down Expand Up @@ -204,6 +205,7 @@ func runServer(stdioMode bool, port int) {
gmailIntegration,
homeassistant.New(),
jira.New(),
mixpanel.New(),
notionInt.New(),
gcpInt.New(),
suno.New(),
Expand Down
10 changes: 10 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ var envMapping = map[string]map[string]string{
"access_token": "RWX_ACCESS_TOKEN",
"cli_path": "RWX_CLI_PATH",
},
"mixpanel": {
"username": "MIXPANEL_USERNAME",
"secret": "MIXPANEL_SECRET",
"project_id": "MIXPANEL_PROJECT_ID",
"base_url": "MIXPANEL_URL",
},
}

// EnvMapping returns the env var mapping table. Useful for documentation and debugging.
Expand Down Expand Up @@ -191,6 +197,10 @@ func defaultConfig() *mcp.Config {
Enabled: false,
Credentials: mcp.Credentials{"cookies": "", "domain": ""},
},
"mixpanel": {
Enabled: false,
Credentials: mcp.Credentials{"username": "", "secret": "", "project_id": "", "base_url": ""},
},
},
}
}
Expand Down
12 changes: 6 additions & 6 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ func TestLoad_CreatesDefaultWhenMissing(t *testing.T) {
_, err = os.Stat(path)
assert.NoError(t, err)

assert.Len(t, m.cfg.Integrations, 20)
for _, name := range []string{"github", "datadog", "linear", "sentry", "slack", "metabase", "aws", "posthog", "postgres", "clickhouse", "pganalyze", "rwx", "gmail", "homeassistant", "notion", "ynab", "gcp", "suno", "amazon", "jira"} {
assert.Len(t, m.cfg.Integrations, 21)
for _, name := range []string{"github", "datadog", "linear", "sentry", "slack", "metabase", "aws", "posthog", "postgres", "clickhouse", "pganalyze", "rwx", "gmail", "homeassistant", "notion", "ynab", "gcp", "suno", "amazon", "jira", "mixpanel"} {
ic, ok := m.cfg.Integrations[name]
assert.True(t, ok, "missing default integration: %s", name)
assert.False(t, ic.Enabled)
Expand Down Expand Up @@ -112,7 +112,7 @@ func TestSave(t *testing.T) {

var cfg mcp.Config
require.NoError(t, json.Unmarshal(data, &cfg))
assert.Len(t, cfg.Integrations, 20)
assert.Len(t, cfg.Integrations, 21)
}

func TestGet(t *testing.T) {
Expand All @@ -121,7 +121,7 @@ func TestGet(t *testing.T) {

cfg := m.Get()
assert.NotNil(t, cfg)
assert.Len(t, cfg.Integrations, 20)
assert.Len(t, cfg.Integrations, 21)
}

func TestUpdate(t *testing.T) {
Expand Down Expand Up @@ -231,7 +231,7 @@ func TestEnabledIntegrations_Multiple(t *testing.T) {
func TestDefaultConfig(t *testing.T) {
cfg := defaultConfig()
require.NotNil(t, cfg)
assert.Len(t, cfg.Integrations, 20)
assert.Len(t, cfg.Integrations, 21)

expected := map[string][]string{
"github": {"token", "client_id", "token_source"},
Expand Down Expand Up @@ -456,7 +456,7 @@ func TestEnvMapping_ReturnsMapping(t *testing.T) {
assert.Equal(t, "DATABASE_URL", m["postgres"]["connection_string"])
assert.Equal(t, "RWX_ACCESS_TOKEN", m["rwx"]["access_token"])
assert.Equal(t, "RWX_CLI_PATH", m["rwx"]["cli_path"])
assert.Len(t, m, 11)
assert.Len(t, m, 12)
assert.Equal(t, "JIRA_EMAIL", m["jira"]["email"])
assert.Equal(t, "JIRA_API_TOKEN", m["jira"]["api_token"])
assert.Equal(t, "JIRA_DOMAIN", m["jira"]["domain"])
Expand Down
58 changes: 58 additions & 0 deletions integrations/mixpanel/compact_specs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package mixpanel

import (
"fmt"

mcp "github.com/daltoniam/switchboard"
)

var rawFieldCompactionSpecs = map[string][]string{
// All Mixpanel Query API endpoints return JSON objects with varying structures.
// All tools are read-only queries.

// ── Insights ────────────────────────────────────────────────────
// Returns the computed results of a saved report
"mixpanel_query_insights": {"results", "series", "date_range"},

// ── Funnels ─────────────────────────────────────────────────────
// Returns {"meta": {...}, "data": {"<date>": {"steps": [...], "analysis": {...}}}}
"mixpanel_query_funnels": {"meta", "data"},

// ── Retention ───────────────────────────────────────────────────
// Returns {"results": {...}} with cohort retention data
"mixpanel_query_retention": {"results"},

// ── Segmentation ────────────────────────────────────────────────
// Returns {"data": {"series": [...], "values": {...}}, "legend_size": N}
"mixpanel_query_segmentation": {"data.series", "data.values", "legend_size"},

// ── Event Properties ────────────────────────────────────────────
// Returns {"data": {"series": [...], "values": {...}}, "legend_size": N}
"mixpanel_query_event_properties": {"data.series", "data.values", "legend_size"},

// ── Profiles (Engage) ───────────────────────────────────────────
// Returns {"page", "page_size", "session_id", "status", "total", "results": [...]}
"mixpanel_query_profiles": {
"results[].$distinct_id",
"results[].$properties",
"page",
"page_size",
"session_id",
"status",
"total",
},
}

var fieldCompactionSpecs = mustBuildFieldCompactionSpecs(rawFieldCompactionSpecs)

func mustBuildFieldCompactionSpecs(raw map[string][]string) map[string][]mcp.CompactField {
parsed := make(map[string][]mcp.CompactField, len(raw))
for tool, specs := range raw {
fields, err := mcp.ParseCompactSpecs(specs)
if err != nil {
panic(fmt.Sprintf("mixpanel: invalid field compaction spec for %q: %v", tool, err))
}
parsed[tool] = fields
}
return parsed
}
46 changes: 46 additions & 0 deletions integrations/mixpanel/compact_specs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package mixpanel

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestFieldCompactionSpecs_AllParse(t *testing.T) {
require.NotEmpty(t, fieldCompactionSpecs, "fieldCompactionSpecs should not be empty")
}

func TestFieldCompactionSpecs_NoDuplicateTools(t *testing.T) {
assert.Equal(t, len(rawFieldCompactionSpecs), len(fieldCompactionSpecs))
}

func TestFieldCompactionSpecs_NoOrphanSpecs(t *testing.T) {
for toolName := range fieldCompactionSpecs {
_, ok := dispatch[toolName]
assert.True(t, ok, "field compaction spec for %q has no dispatch handler", toolName)
}
}

func TestFieldCompactionSpecs_OnlyReadTools(t *testing.T) {
mutationPrefixes := []string{"create", "update", "delete"}
for toolName := range fieldCompactionSpecs {
for _, prefix := range mutationPrefixes {
assert.NotContains(t, toolName, "_"+prefix+"_",
"mutation tool %q should not have a field compaction spec", toolName)
}
}
}

func TestFieldCompactionSpec_ReturnsFieldsForQueryTool(t *testing.T) {
m := &mixpanel{}
fields, ok := m.CompactSpec("mixpanel_query_profiles")
require.True(t, ok, "mixpanel_query_profiles should have field compaction spec")
assert.NotEmpty(t, fields)
}

func TestFieldCompactionSpec_ReturnsFalseForUnknownTool(t *testing.T) {
m := &mixpanel{}
_, ok := m.CompactSpec("mixpanel_nonexistent")
assert.False(t, ok, "unknown tools should return false")
}
Loading