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 @@ -17,6 +17,7 @@ import (
"github.com/daltoniam/switchboard/daemon"
"github.com/daltoniam/switchboard/datadog"
"github.com/daltoniam/switchboard/github"
"github.com/daltoniam/switchboard/instagram"
"github.com/daltoniam/switchboard/linear"
"github.com/daltoniam/switchboard/metabase"
"github.com/daltoniam/switchboard/posthog"
Expand Down Expand Up @@ -173,6 +174,7 @@ func runServer(stdioMode bool, port int) {
awsInt.New(),
posthog.New(),
postgres.New(),
instagram.New(),
} {
if err := reg.Register(i); err != nil {
log.Fatalf("Failed to register integration: %v", err)
Expand Down
4 changes: 4 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ func defaultConfig() *mcp.Config {
Enabled: false,
Credentials: mcp.Credentials{"connection_string": "", "host": "", "port": "", "user": "", "password": "", "database": "", "sslmode": "", "read_only": ""},
},
"instagram": {
Enabled: false,
Credentials: mcp.Credentials{"access_token": "", "user_id": "", "api_version": ""},
},
},
}
}
Expand Down
29 changes: 15 additions & 14 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ func TestLoad_CreatesDefaultWhenMissing(t *testing.T) {
_, err = os.Stat(path)
assert.NoError(t, err)

assert.Len(t, m.cfg.Integrations, 9)
for _, name := range []string{"github", "datadog", "linear", "sentry", "slack", "metabase", "aws", "posthog", "postgres"} {
assert.Len(t, m.cfg.Integrations, 10)
for _, name := range []string{"github", "datadog", "linear", "sentry", "slack", "metabase", "aws", "posthog", "postgres", "instagram"} {
ic, ok := m.cfg.Integrations[name]
assert.True(t, ok, "missing default integration: %s", name)
assert.False(t, ic.Enabled)
Expand Down Expand Up @@ -80,7 +80,7 @@ func TestSave(t *testing.T) {

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

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

cfg := m.Get()
assert.NotNil(t, cfg)
assert.Len(t, cfg.Integrations, 9)
assert.Len(t, cfg.Integrations, 10)
}

func TestUpdate(t *testing.T) {
Expand Down Expand Up @@ -199,18 +199,19 @@ func TestEnabledIntegrations_Multiple(t *testing.T) {
func TestDefaultConfig(t *testing.T) {
cfg := defaultConfig()
require.NotNil(t, cfg)
assert.Len(t, cfg.Integrations, 9)
assert.Len(t, cfg.Integrations, 10)

expected := map[string][]string{
"github": {"token", "client_id", "token_source"},
"datadog": {"api_key", "app_key"},
"linear": {"api_key", "client_id", "client_secret", "token_source"},
"sentry": {"auth_token", "organization", "client_id", "token_source"},
"slack": {"token", "cookie"},
"metabase": {"api_key", "url"},
"aws": {"access_key_id", "secret_access_key", "session_token", "region"},
"posthog": {"api_key", "project_id", "base_url"},
"postgres": {"connection_string", "host", "user", "read_only"},
"github": {"token", "client_id", "token_source"},
"datadog": {"api_key", "app_key"},
"linear": {"api_key", "client_id", "client_secret", "token_source"},
"sentry": {"auth_token", "organization", "client_id", "token_source"},
"slack": {"token", "cookie"},
"metabase": {"api_key", "url"},
"aws": {"access_key_id", "secret_access_key", "session_token", "region"},
"posthog": {"api_key", "project_id", "base_url"},
"postgres": {"connection_string", "host", "user", "read_only"},
"instagram": {"access_token", "user_id"},
}

for name, keys := range expected {
Expand Down
184 changes: 184 additions & 0 deletions instagram/extras.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package instagram

import (
"context"
"fmt"

mcp "github.com/daltoniam/switchboard"
)

// ── Comments ────────────────────────────────────────────────────────

func listComments(ctx context.Context, ig *instagram, args map[string]any) (*mcp.ToolResult, error) {
mediaID := argStr(args, "media_id")
fields := argStr(args, "fields")
if fields == "" {
fields = "id,text,timestamp,username,like_count"
}
q := "fields=" + fields
q += queryEncode(map[string]string{
"limit": argStr(args, "limit"),
"after": argStr(args, "after"),
})
data, err := ig.get(ctx, "/%s/comments?%s", mediaID, q)
if err != nil {
return errResult(err)
}
return rawResult(data)
}

func getComment(ctx context.Context, ig *instagram, args map[string]any) (*mcp.ToolResult, error) {
commentID := argStr(args, "comment_id")
fields := argStr(args, "fields")
if fields == "" {
fields = "id,text,timestamp,username,like_count"
}
data, err := ig.get(ctx, "/%s?fields=%s", commentID, fields)
if err != nil {
return errResult(err)
}
return rawResult(data)
}

func replyToComment(ctx context.Context, ig *instagram, args map[string]any) (*mcp.ToolResult, error) {
mediaID := argStr(args, "media_id")
message := argStr(args, "message")
path := fmt.Sprintf("/%s/comments?message=%s", mediaID, message)
data, err := ig.post(ctx, path, nil)
if err != nil {
return errResult(err)
}
return rawResult(data)
}

func listCommentReplies(ctx context.Context, ig *instagram, args map[string]any) (*mcp.ToolResult, error) {
commentID := argStr(args, "comment_id")
fields := argStr(args, "fields")
if fields == "" {
fields = "id,text,timestamp,username"
}
q := "fields=" + fields
q += queryEncode(map[string]string{
"limit": argStr(args, "limit"),
})
data, err := ig.get(ctx, "/%s/replies?%s", commentID, q)
if err != nil {
return errResult(err)
}
return rawResult(data)
}

func hideComment(ctx context.Context, ig *instagram, args map[string]any) (*mcp.ToolResult, error) {
commentID := argStr(args, "comment_id")
hide := true
if v, ok := args["hide"]; ok {
hide = argBool(map[string]any{"hide": v}, "hide")
}
path := fmt.Sprintf("/%s?hide=%t", commentID, hide)
data, err := ig.post(ctx, path, nil)
if err != nil {
return errResult(err)
}
return rawResult(data)
}

func deleteComment(ctx context.Context, ig *instagram, args map[string]any) (*mcp.ToolResult, error) {
commentID := argStr(args, "comment_id")
data, err := ig.del(ctx, "/%s", commentID)
if err != nil {
return errResult(err)
}
return rawResult(data)
}

func getMentionedComment(ctx context.Context, ig *instagram, args map[string]any) (*mcp.ToolResult, error) {
commentID := argStr(args, "comment_id")
fields := argStr(args, "fields")
if fields == "" {
fields = "id,text,timestamp"
}
path := fmt.Sprintf("/%s/mentioned_comment?comment_id=%s&fields=%s", ig.uid(args), commentID, fields)
data, err := ig.get(ctx, "%s", path)
if err != nil {
return errResult(err)
}
return rawResult(data)
}

func getMentionedMedia(ctx context.Context, ig *instagram, args map[string]any) (*mcp.ToolResult, error) {
mediaID := argStr(args, "media_id")
fields := argStr(args, "fields")
if fields == "" {
fields = "id,caption,media_type,timestamp"
}
path := fmt.Sprintf("/%s/mentioned_media?media_id=%s&fields=%s", ig.uid(args), mediaID, fields)
data, err := ig.get(ctx, "%s", path)
if err != nil {
return errResult(err)
}
return rawResult(data)
}

// ── Insights ────────────────────────────────────────────────────────

func getMediaInsights(ctx context.Context, ig *instagram, args map[string]any) (*mcp.ToolResult, error) {
mediaID := argStr(args, "media_id")
metric := argStr(args, "metric")
data, err := ig.get(ctx, "/%s/insights?metric=%s", mediaID, metric)
if err != nil {
return errResult(err)
}
return rawResult(data)
}

func getAccountInsights(ctx context.Context, ig *instagram, args map[string]any) (*mcp.ToolResult, error) {
metric := argStr(args, "metric")
period := argStr(args, "period")
q := fmt.Sprintf("metric=%s&period=%s", metric, period)
q += queryEncode(map[string]string{
"since": argStr(args, "since"),
"until": argStr(args, "until"),
})
data, err := ig.get(ctx, "/%s/insights?%s", ig.uid(args), q)
if err != nil {
return errResult(err)
}
return rawResult(data)
}

// ── Hashtags ────────────────────────────────────────────────────────

func searchHashtag(ctx context.Context, ig *instagram, args map[string]any) (*mcp.ToolResult, error) {
q := argStr(args, "q")
data, err := ig.get(ctx, "/ig_hashtag_search?user_id=%s&q=%s", ig.uid(args), q)
if err != nil {
return errResult(err)
}
return rawResult(data)
}

func getHashtagRecent(ctx context.Context, ig *instagram, args map[string]any) (*mcp.ToolResult, error) {
hashtagID := argStr(args, "hashtag_id")
fields := argStr(args, "fields")
if fields == "" {
fields = "id,caption,media_type,permalink,timestamp"
}
data, err := ig.get(ctx, "/%s/recent_media?user_id=%s&fields=%s", hashtagID, ig.uid(args), fields)
if err != nil {
return errResult(err)
}
return rawResult(data)
}

func getHashtagTop(ctx context.Context, ig *instagram, args map[string]any) (*mcp.ToolResult, error) {
hashtagID := argStr(args, "hashtag_id")
fields := argStr(args, "fields")
if fields == "" {
fields = "id,caption,media_type,permalink,timestamp"
}
data, err := ig.get(ctx, "/%s/top_media?user_id=%s&fields=%s", hashtagID, ig.uid(args), fields)
if err != nil {
return errResult(err)
}
return rawResult(data)
}
Loading