Compare commits

..

No commits in common. "359e8dbc793d63f202cae1488a55a15732933922" and "8f9efe013581cc8c30e7f9257148009f301f8c36" have entirely different histories.

10 changed files with 34 additions and 96 deletions

View file

@ -30,7 +30,6 @@ The server is a bridge between the Telegram API and the AI assistants and is bas
- [Configuration](#configuration) - [Configuration](#configuration)
- [Authorization](#authorization) - [Authorization](#authorization)
- [Client Configuration](#client-configuration) - [Client Configuration](#client-configuration)
- [JSON Schema Version](#json-schema-version)
- [Star History](#star-history) - [Star History](#star-history)
## What is MCP? ## What is MCP?
@ -264,16 +263,6 @@ Example of Configuring Claude Desktop to recognize the Telegram MCP server.
} }
``` ```
### JSON Schema Version
Some MCP clients (e.g. VS Code) do not support JSON Schema Draft 2020-12 and will reject tools that use it. You can override the JSON Schema version by setting the `--schema-version` flag or the `TG_SCHEMA_VERSION` environment variable.
Common values:
| Version | URL |
|---------|-----|
| Draft-07 (recommended for VS Code) | `https://json-schema.org/draft-07/schema#` |
| Draft 2020-12 (default) | `https://json-schema.org/draft/2020-12/schema` |
## Star History ## Star History
<a href="https://www.star-history.com/#chaindead/telegram-mcp&Date"> <a href="https://www.star-history.com/#chaindead/telegram-mcp&Date">

2
go.mod
View file

@ -4,7 +4,6 @@ go 1.24
require ( require (
github.com/gotd/td v0.121.0 github.com/gotd/td v0.121.0
github.com/invopop/jsonschema v0.12.0
github.com/metoro-io/mcp-golang v0.8.0 github.com/metoro-io/mcp-golang v0.8.0
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.34.0
@ -29,6 +28,7 @@ require (
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gotd/ige v0.2.2 // indirect github.com/gotd/ige v0.2.2 // indirect
github.com/gotd/neo v0.1.5 // indirect github.com/gotd/neo v0.1.5 // indirect
github.com/invopop/jsonschema v0.12.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect

View file

@ -17,12 +17,10 @@ func New(appID int, appHash, sessionPath string) *Client {
} }
func (c *Client) T() *telegram.Client { func (c *Client) T() *telegram.Client {
opts := telegram.Options{ return telegram.NewClient(c.appID, c.appHash, telegram.Options{
SessionStorage: &telegram.FileSessionStorage{ SessionStorage: &telegram.FileSessionStorage{
Path: c.sessionPath, Path: c.sessionPath,
}, },
NoUpdates: true, NoUpdates: true,
} })
opts, _ = telegram.OptionsFromEnvironment(opts)
return telegram.NewClient(c.appID, c.appHash, opts)
} }

View file

@ -257,6 +257,7 @@ func (d *dialogs) processDialog(dialogItem *tg.Dialog) (DialogInfo, error) {
Text: text, Text: text,
IsUnread: dialogItem.UnreadCount > 0, IsUnread: dialogItem.UnreadCount > 0,
} }
} }
if dialogItem.Peer == nil { if dialogItem.Peer == nil {

View file

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/gotd/td/telegram/message"
"github.com/gotd/td/tg" "github.com/gotd/td/tg"
mcp "github.com/metoro-io/mcp-golang" mcp "github.com/metoro-io/mcp-golang"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -25,9 +26,10 @@ func (c *Client) SendDraft(args DraftArguments) (*mcp.ToolResponse, error) {
if err := client.Run(context.Background(), func(ctx context.Context) (err error) { if err := client.Run(context.Background(), func(ctx context.Context) (err error) {
api := client.API() api := client.API()
inputPeer, err := getInputPeerFromName(ctx, api, args.Name) sender := message.NewSender(api)
inputPeer, err := sender.Resolve(args.Name).AsInputPeer(ctx)
if err != nil { if err != nil {
return fmt.Errorf("get inputPeer from name: %w", err) return fmt.Errorf("failed to resolve name: %w", err)
} }
ok, err = api.MessagesSaveDraft(ctx, &tg.MessagesSaveDraftRequest{ ok, err = api.MessagesSaveDraft(ctx, &tg.MessagesSaveDraftRequest{

View file

@ -32,12 +32,9 @@ func getUsername(source any) string {
case *tg.User: case *tg.User:
username = u.Username username = u.Username
case *tg.Chat: case *tg.Chat:
username = fmt.Sprintf("cht[%d]", u.ID) username = fmt.Sprintf("chat(%d)", u.ID)
case *tg.Channel: case *tg.Channel:
username = u.Username username = u.Username
if username == "" {
username = fmt.Sprintf("chn[%d:%d]", u.ID, u.AccessHash)
}
} }
return username return username

View file

@ -4,7 +4,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/gotd/td/telegram/message" "github.com/gotd/td/telegram/message"
@ -29,9 +28,10 @@ func (c *Client) GetHistory(args HistoryArguments) (*mcp.ToolResponse, error) {
if err := client.Run(context.Background(), func(ctx context.Context) (err error) { if err := client.Run(context.Background(), func(ctx context.Context) (err error) {
api := client.API() api := client.API()
inputPeer, err := getInputPeerFromName(ctx, api, args.Name) sender := message.NewSender(api)
inputPeer, err := sender.Resolve(args.Name).AsInputPeer(ctx)
if err != nil { if err != nil {
return fmt.Errorf("get inputPeer from name: %w", err) return fmt.Errorf("failed to resolve name: %w", err)
} }
messagesClass, err = api.MessagesGetHistory(ctx, &tg.MessagesGetHistoryRequest{ messagesClass, err = api.MessagesGetHistory(ctx, &tg.MessagesGetHistoryRequest{
@ -69,37 +69,6 @@ func (c *Client) GetHistory(args HistoryArguments) (*mcp.ToolResponse, error) {
return mcp.NewToolResponse(mcp.NewTextContent(string(jsonData))), nil return mcp.NewToolResponse(mcp.NewTextContent(string(jsonData))), nil
} }
func getInputPeerFromName(ctx context.Context, api *tg.Client, name string) (tg.InputPeerClass, error) {
isCustom := strings.Contains(name, "[") && strings.Contains(name, "]")
switch {
case strings.HasPrefix(name, "chn") && isCustom:
var channelPeer tg.InputPeerChannel
_, err := fmt.Sscanf(name, "chn[%d:%d]", &channelPeer.ChannelID, &channelPeer.AccessHash)
if err != nil {
return nil, errors.Wrapf(err, "scan channel peer(%q)", name)
}
return &channelPeer, nil
case strings.HasPrefix(name, "cht") && isCustom:
var chatPeer tg.InputPeerChat
_, err := fmt.Sscanf(name, "cht[%d]", &chatPeer.ChatID)
if err != nil {
return nil, errors.Wrapf(err, "scan chat peer(%q)", name)
}
return &chatPeer, nil
default:
sender := message.NewSender(api)
inputPeer, err := sender.Resolve(name).AsInputPeer(ctx)
if err != nil {
return nil, fmt.Errorf("failed to resolve name: %w", err)
}
return inputPeer, nil
}
}
type history struct { type history struct {
tg.MessagesMessages tg.MessagesMessages
users map[int64]*tg.User users map[int64]*tg.User

View file

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/gotd/td/telegram/message"
"github.com/gotd/td/tg" "github.com/gotd/td/tg"
mcp "github.com/metoro-io/mcp-golang" mcp "github.com/metoro-io/mcp-golang"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -26,13 +27,14 @@ func (c *Client) ReadHistory(args ReadArguments) (*mcp.ToolResponse, error) {
if err := client.Run(ctx, func(ctx context.Context) error { if err := client.Run(ctx, func(ctx context.Context) error {
api := client.API() api := client.API()
inputPeer, err := getInputPeerFromName(ctx, api, args.Name) sender := message.NewSender(api)
inputPeer, err := sender.Resolve(args.Name).AsInputPeer(ctx)
if err != nil { if err != nil {
return fmt.Errorf("get inputPeer from name: %w", err) return fmt.Errorf("failed to resolve name: %w", err)
} }
switch p := inputPeer.(type) { switch p := inputPeer.(type) {
case *tg.InputPeerUser, *tg.InputPeerChat: case *tg.InputPeerUser:
affectedMsgs, err = api.MessagesReadHistory(ctx, &tg.MessagesReadHistoryRequest{ affectedMsgs, err = api.MessagesReadHistory(ctx, &tg.MessagesReadHistoryRequest{
Peer: inputPeer, Peer: inputPeer,
}) })
@ -56,6 +58,7 @@ func (c *Client) ReadHistory(args ReadArguments) (*mcp.ToolResponse, error) {
PtsCount: 1, PtsCount: 1,
} }
} }
default: default:
return fmt.Errorf("unexpected input peer type: %T", p) return fmt.Errorf("unexpected input peer type: %T", p)
} }

View file

@ -59,11 +59,6 @@ func main() {
Value: sesionPath, Value: sesionPath,
Sources: cli.EnvVars("TG_SESSION_PATH"), Sources: cli.EnvVars("TG_SESSION_PATH"),
}, },
&cli.StringFlag{
Name: "schema-version",
Usage: "JSON Schema version URL (e.g. https://json-schema.org/draft-07/schema#)",
Sources: cli.EnvVars("TG_SCHEMA_VERSION"),
},
&cli.BoolFlag{ &cli.BoolFlag{
Name: "dry", Name: "dry",
Usage: "Test configuration", Usage: "Test configuration",

View file

@ -7,7 +7,6 @@ import (
"os" "os"
"github.com/chaindead/telegram-mcp/internal/tg" "github.com/chaindead/telegram-mcp/internal/tg"
"github.com/invopop/jsonschema"
mcp "github.com/metoro-io/mcp-golang" mcp "github.com/metoro-io/mcp-golang"
"github.com/metoro-io/mcp-golang/transport/stdio" "github.com/metoro-io/mcp-golang/transport/stdio"
@ -21,10 +20,6 @@ func serve(ctx context.Context, cmd *cli.Command) error {
sessionPath := cmd.String("session") sessionPath := cmd.String("session")
dryRun := cmd.Bool("dry") dryRun := cmd.Bool("dry")
if schemaURL := cmd.String("schema-version"); schemaURL != "" {
jsonschema.Version = schemaURL
}
_, err := os.Stat(sessionPath) _, err := os.Stat(sessionPath)
if err != nil { if err != nil {
return fmt.Errorf("session file not found(%s): %w", sessionPath, err) return fmt.Errorf("session file not found(%s): %w", sessionPath, err)
@ -55,35 +50,24 @@ func serve(ctx context.Context, cmd *cli.Command) error {
answer, err = client.GetHistory(tg.HistoryArguments{Name: os.Getenv("TG_TEST_USERNAME")}) answer, err = client.GetHistory(tg.HistoryArguments{Name: os.Getenv("TG_TEST_USERNAME")})
if err != nil { if err != nil {
return fmt.Errorf("get nickname history: %w", err) return fmt.Errorf("get histore: %w", err)
}
answer, err = client.GetHistory(tg.HistoryArguments{Name: "cht[4626931529]"})
if err != nil {
return fmt.Errorf("get chat history: %w", err)
}
answer, err = client.GetHistory(tg.HistoryArguments{Name: "chn[2225853048:8934705438195741763]"})
if err != nil {
return fmt.Errorf("get chan history: %w", err)
} }
log.Info().RawJSON("answer", []byte(answer.Content[0].TextContent.Text)).Msg("Check GetHistory: OK") log.Info().RawJSON("answer", []byte(answer.Content[0].TextContent.Text)).Msg("Check GetHistory: OK")
// Disabled write operations in dry run mode for safety answer, err = client.SendDraft(tg.DraftArguments{Name: os.Getenv("TG_TEST_USERNAME"), Text: "test draft"})
// answer, err = client.SendDraft(tg.DraftArguments{Name: os.Getenv("TG_TEST_USERNAME"), Text: "test draft"}) if err != nil {
// if err != nil { log.Err(err).Msg("Check SendDraft: FAIL")
// log.Err(err).Msg("Check SendDraft: FAIL") } else {
// } else { log.Info().RawJSON("answer", []byte(answer.Content[0].TextContent.Text)).Msg("Check SendDraft: OK")
// log.Info().RawJSON("answer", []byte(answer.Content[0].TextContent.Text)).Msg("Check SendDraft: OK") }
// }
// answer, err = client.ReadHistory(tg.ReadArguments{Name: os.Getenv("TG_TEST_USERNAME")}) answer, err = client.ReadHistory(tg.ReadArguments{Name: os.Getenv("TG_TEST_USERNAME")})
// if err != nil { if err != nil {
// log.Err(err).Msg("Check ReadHistory: FAIL") log.Err(err).Msg("Check ReadHistory: FAIL")
// } else { } else {
// log.Info().RawJSON("answer", []byte(answer.Content[0].TextContent.Text)).Msg("Check ReadHistory: OK") log.Info().RawJSON("answer", []byte(answer.Content[0].TextContent.Text)).Msg("Check ReadHistory: OK")
// } }
return nil return nil
} }
@ -98,17 +82,17 @@ func serve(ctx context.Context, cmd *cli.Command) error {
return fmt.Errorf("register dialogs tool: %w", err) return fmt.Errorf("register dialogs tool: %w", err)
} }
err = server.RegisterTool("tg_dialog", "Get messages of telegram dialog", client.GetHistory) err = server.RegisterTool("tg_dialog", "Get messages of telegram dialog (channel, user)", client.GetHistory)
if err != nil { if err != nil {
return fmt.Errorf("register dialogs tool: %w", err) return fmt.Errorf("register dialogs tool: %w", err)
} }
err = server.RegisterTool("tg_send", "Send draft message to dialog", client.SendDraft) err = server.RegisterTool("tg_send", "Send draft message to dialog (channel, user)", client.SendDraft)
if err != nil { if err != nil {
return fmt.Errorf("register dialogs tool: %w", err) return fmt.Errorf("register dialogs tool: %w", err)
} }
err = server.RegisterTool("tg_read", "Mark dialog messages as read", client.ReadHistory) err = server.RegisterTool("tg_read", "Mark dialog messages as read (channel, user)", client.ReadHistory)
if err != nil { if err != nil {
return fmt.Errorf("register read tool: %w", err) return fmt.Errorf("register read tool: %w", err)
} }