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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
2 changes: 2 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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(),
Expand Down
7 changes: 7 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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: ""},
Expand Down
53 changes: 53 additions & 0 deletions integrations/intercom/intercom.go
Original file line number Diff line number Diff line change
@@ -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)
}
63 changes: 63 additions & 0 deletions integrations/intercom/intercom_test.go
Original file line number Diff line number Diff line change
@@ -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)
}