From 72b454d6093c97bc56bd928949d51aa782b6d7db Mon Sep 17 00:00:00 2001 From: Paul Goergen Date: Fri, 22 May 2026 10:00:30 -0700 Subject: [PATCH] Add Intercom integration Proxy Intercom's official remote MCP server at https://mcp.intercom.com via the existing remotemcp package. Mirrors the Linear and pganalyze patterns so the integration auto-prefixes intercom_* on tool names and delegates configure / list / execute to the upstream server. - integrations/intercom: ~50-line wrapper around remotemcp.New plus tests - cmd/server/main.go: register intercom.New() in the integration list - config/config.go: defaultConfig entry + INTERCOM_ACCESS_TOKEN env mapping - README.md: env-var table entry Credentials: workspace Access Token under "access_token". OAuth path is not wired yet (Intercom MCP supports both, but PAT-style is sufficient for first-party data access). Co-Authored-By: Claude Opus 4.7 Co-authored-by: Cursor --- README.md | 1 + cmd/server/main.go | 2 + config/config.go | 7 +++ integrations/intercom/intercom.go | 53 ++++++++++++++++++++++ integrations/intercom/intercom_test.go | 63 ++++++++++++++++++++++++++ 5 files changed, 126 insertions(+) create mode 100644 integrations/intercom/intercom.go create mode 100644 integrations/intercom/intercom_test.go diff --git a/README.md b/README.md index 7d9941f..f906078 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ Any integration with credentials provided via env vars will auto-enable without | Datadog | `app_key` | `DD_APP_KEY` | | Datadog | `site` | `DD_SITE` | | Linear | `api_key` | `LINEAR_API_KEY` | +| Intercom | `access_token` | `INTERCOM_ACCESS_TOKEN` | | Sentry | `auth_token` | `SENTRY_AUTH_TOKEN` | | Sentry | `organization` | `SENTRY_ORG` (optional — auto-detected from API) | | Slack | `token` | `SLACK_TOKEN` | diff --git a/cmd/server/main.go b/cmd/server/main.go index 6fd7465..a6b7b56 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -42,6 +42,7 @@ import ( "github.com/daltoniam/switchboard/integrations/gsheets" "github.com/daltoniam/switchboard/integrations/gslides" "github.com/daltoniam/switchboard/integrations/gtasks" + "github.com/daltoniam/switchboard/integrations/intercom" "github.com/daltoniam/switchboard/integrations/jira" "github.com/daltoniam/switchboard/integrations/linear" "github.com/daltoniam/switchboard/integrations/metabase" @@ -233,6 +234,7 @@ func runServer(stdioMode bool, port int, discoverAll bool) { github.New(), datadog.New(), linear.New("https://mcp.linear.app"), + intercom.New(), sentry.New(), slackInt.New(), metabase.New(), diff --git a/config/config.go b/config/config.go index 8269d7d..ca59bb2 100644 --- a/config/config.go +++ b/config/config.go @@ -30,6 +30,9 @@ var envMapping = map[string]map[string]string{ "linear": { "api_key": "LINEAR_API_KEY", }, + "intercom": { + "access_token": "INTERCOM_ACCESS_TOKEN", + }, "sentry": { "auth_token": "SENTRY_AUTH_TOKEN", "organization": "SENTRY_ORG", @@ -202,6 +205,10 @@ func defaultConfig() *mcp.Config { Enabled: false, Credentials: mcp.Credentials{"api_key": "", "mcp_access_token": "", mcp.CredKeyTokenSource: ""}, }, + "intercom": { + Enabled: false, + Credentials: mcp.Credentials{"access_token": ""}, + }, "sentry": { Enabled: false, Credentials: mcp.Credentials{"auth_token": "", "organization": "", mcp.CredKeyClientID: "", mcp.CredKeyTokenSource: ""}, diff --git a/integrations/intercom/intercom.go b/integrations/intercom/intercom.go new file mode 100644 index 0000000..836ae15 --- /dev/null +++ b/integrations/intercom/intercom.go @@ -0,0 +1,53 @@ +// Package intercom proxies Intercom's official remote MCP server +// (https://mcp.intercom.com) as a Switchboard integration. All tool +// discovery and execution is delegated to remotemcp; this wrapper exists +// so the integration has a stable Name(), a single registration point in +// cmd/server/main.go, and a seam for future native handlers if Intercom's +// MCP coverage proves insufficient. +package intercom + +import ( + "context" + + mcp "github.com/daltoniam/switchboard" + "github.com/daltoniam/switchboard/remotemcp" +) + +// defaultMCPServerURL is Intercom's hosted MCP endpoint. remotemcp +// appends "/mcp" automatically when connecting. +const defaultMCPServerURL = "https://mcp.intercom.com" + +var _ mcp.Integration = (*intercom)(nil) + +type intercom struct { + remote mcp.Integration +} + +// New creates an Intercom integration backed by Intercom's official MCP +// server. An optional URL override is accepted to support tests and +// custom endpoints; production callers should pass nothing. +func New(mcpServerURL ...string) mcp.Integration { + url := defaultMCPServerURL + if len(mcpServerURL) > 0 && mcpServerURL[0] != "" { + url = mcpServerURL[0] + } + return &intercom{remote: remotemcp.New("intercom", url)} +} + +func (i *intercom) Name() string { return "intercom" } + +func (i *intercom) Configure(ctx context.Context, creds mcp.Credentials) error { + return i.remote.Configure(ctx, creds) +} + +func (i *intercom) Healthy(ctx context.Context) bool { + return i.remote.Healthy(ctx) +} + +func (i *intercom) Tools() []mcp.ToolDefinition { + return i.remote.Tools() +} + +func (i *intercom) Execute(ctx context.Context, toolName mcp.ToolName, args map[string]any) (*mcp.ToolResult, error) { + return i.remote.Execute(ctx, toolName, args) +} diff --git a/integrations/intercom/intercom_test.go b/integrations/intercom/intercom_test.go new file mode 100644 index 0000000..c4af2ce --- /dev/null +++ b/integrations/intercom/intercom_test.go @@ -0,0 +1,63 @@ +package intercom + +import ( + "context" + "testing" + + mcp "github.com/daltoniam/switchboard" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNew_DefaultServer(t *testing.T) { + i := New() + require.NotNil(t, i) + assert.Equal(t, "intercom", i.Name()) +} + +func TestNew_OverrideServerURL(t *testing.T) { + i := New("https://custom.example.com") + require.NotNil(t, i) + assert.Equal(t, "intercom", i.Name()) +} + +func TestNew_EmptyOverrideUsesDefault(t *testing.T) { + i := New("") + require.NotNil(t, i) + assert.Equal(t, "intercom", i.Name()) +} + +func TestConfigure_DelegatesAccessToken(t *testing.T) { + i := New("https://example.com") + err := i.Configure(context.Background(), mcp.Credentials{"access_token": "dG9rOmFiYw=="}) + assert.NoError(t, err) +} + +func TestConfigure_MissingAccessToken(t *testing.T) { + i := New("https://example.com") + err := i.Configure(context.Background(), mcp.Credentials{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "access_token") +} + +func TestConfigure_EmptyAccessToken(t *testing.T) { + i := New("https://example.com") + err := i.Configure(context.Background(), mcp.Credentials{"access_token": ""}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "access_token") +} + +func TestHealthy_NoConfigure(t *testing.T) { + i := New("https://example.com") + assert.False(t, i.Healthy(context.Background())) +} + +func TestExecute_NoConnection(t *testing.T) { + i := New("https://invalid.example.com") + require.NoError(t, i.Configure(context.Background(), mcp.Credentials{"access_token": "tok"})) + + result, err := i.Execute(context.Background(), mcp.ToolName("intercom_search_conversations"), nil) + assert.NoError(t, err) + require.NotNil(t, result) + assert.True(t, result.IsError) +}