Compare commits
10 commits
8f9efe0135
...
359e8dbc79
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
359e8dbc79 | ||
|
|
1c2c03a792 | ||
|
|
764883677e | ||
|
|
ba919808e0 | ||
|
|
5bd757599d | ||
|
|
956f664f17 | ||
|
|
2788edab95 | ||
|
|
3d09fff50e | ||
|
|
98b3e1da4c | ||
|
|
911e761430 |
10 changed files with 96 additions and 34 deletions
11
README.md
11
README.md
|
|
@ -30,6 +30,7 @@ The server is a bridge between the Telegram API and the AI assistants and is bas
|
|||
- [Configuration](#configuration)
|
||||
- [Authorization](#authorization)
|
||||
- [Client Configuration](#client-configuration)
|
||||
- [JSON Schema Version](#json-schema-version)
|
||||
- [Star History](#star-history)
|
||||
|
||||
## What is MCP?
|
||||
|
|
@ -263,6 +264,16 @@ 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
|
||||
|
||||
<a href="https://www.star-history.com/#chaindead/telegram-mcp&Date">
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -4,6 +4,7 @@ go 1.24
|
|||
|
||||
require (
|
||||
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/pkg/errors v0.9.1
|
||||
github.com/rs/zerolog v1.34.0
|
||||
|
|
@ -28,7 +29,6 @@ require (
|
|||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gotd/ige v0.2.2 // 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/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
|
|
|
|||
|
|
@ -17,10 +17,12 @@ func New(appID int, appHash, sessionPath string) *Client {
|
|||
}
|
||||
|
||||
func (c *Client) T() *telegram.Client {
|
||||
return telegram.NewClient(c.appID, c.appHash, telegram.Options{
|
||||
opts := telegram.Options{
|
||||
SessionStorage: &telegram.FileSessionStorage{
|
||||
Path: c.sessionPath,
|
||||
},
|
||||
NoUpdates: true,
|
||||
})
|
||||
}
|
||||
opts, _ = telegram.OptionsFromEnvironment(opts)
|
||||
return telegram.NewClient(c.appID, c.appHash, opts)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -257,7 +257,6 @@ func (d *dialogs) processDialog(dialogItem *tg.Dialog) (DialogInfo, error) {
|
|||
Text: text,
|
||||
IsUnread: dialogItem.UnreadCount > 0,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if dialogItem.Peer == nil {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/gotd/td/telegram/message"
|
||||
"github.com/gotd/td/tg"
|
||||
mcp "github.com/metoro-io/mcp-golang"
|
||||
"github.com/pkg/errors"
|
||||
|
|
@ -26,10 +25,9 @@ func (c *Client) SendDraft(args DraftArguments) (*mcp.ToolResponse, error) {
|
|||
if err := client.Run(context.Background(), func(ctx context.Context) (err error) {
|
||||
api := client.API()
|
||||
|
||||
sender := message.NewSender(api)
|
||||
inputPeer, err := sender.Resolve(args.Name).AsInputPeer(ctx)
|
||||
inputPeer, err := getInputPeerFromName(ctx, api, args.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve name: %w", err)
|
||||
return fmt.Errorf("get inputPeer from name: %w", err)
|
||||
}
|
||||
|
||||
ok, err = api.MessagesSaveDraft(ctx, &tg.MessagesSaveDraftRequest{
|
||||
|
|
|
|||
|
|
@ -32,9 +32,12 @@ func getUsername(source any) string {
|
|||
case *tg.User:
|
||||
username = u.Username
|
||||
case *tg.Chat:
|
||||
username = fmt.Sprintf("chat(%d)", u.ID)
|
||||
username = fmt.Sprintf("cht[%d]", u.ID)
|
||||
case *tg.Channel:
|
||||
username = u.Username
|
||||
if username == "" {
|
||||
username = fmt.Sprintf("chn[%d:%d]", u.ID, u.AccessHash)
|
||||
}
|
||||
}
|
||||
|
||||
return username
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gotd/td/telegram/message"
|
||||
|
|
@ -28,10 +29,9 @@ func (c *Client) GetHistory(args HistoryArguments) (*mcp.ToolResponse, error) {
|
|||
if err := client.Run(context.Background(), func(ctx context.Context) (err error) {
|
||||
api := client.API()
|
||||
|
||||
sender := message.NewSender(api)
|
||||
inputPeer, err := sender.Resolve(args.Name).AsInputPeer(ctx)
|
||||
inputPeer, err := getInputPeerFromName(ctx, api, args.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve name: %w", err)
|
||||
return fmt.Errorf("get inputPeer from name: %w", err)
|
||||
}
|
||||
|
||||
messagesClass, err = api.MessagesGetHistory(ctx, &tg.MessagesGetHistoryRequest{
|
||||
|
|
@ -69,6 +69,37 @@ func (c *Client) GetHistory(args HistoryArguments) (*mcp.ToolResponse, error) {
|
|||
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 {
|
||||
tg.MessagesMessages
|
||||
users map[int64]*tg.User
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/gotd/td/telegram/message"
|
||||
"github.com/gotd/td/tg"
|
||||
mcp "github.com/metoro-io/mcp-golang"
|
||||
"github.com/pkg/errors"
|
||||
|
|
@ -27,14 +26,13 @@ func (c *Client) ReadHistory(args ReadArguments) (*mcp.ToolResponse, error) {
|
|||
if err := client.Run(ctx, func(ctx context.Context) error {
|
||||
api := client.API()
|
||||
|
||||
sender := message.NewSender(api)
|
||||
inputPeer, err := sender.Resolve(args.Name).AsInputPeer(ctx)
|
||||
inputPeer, err := getInputPeerFromName(ctx, api, args.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve name: %w", err)
|
||||
return fmt.Errorf("get inputPeer from name: %w", err)
|
||||
}
|
||||
|
||||
switch p := inputPeer.(type) {
|
||||
case *tg.InputPeerUser:
|
||||
case *tg.InputPeerUser, *tg.InputPeerChat:
|
||||
affectedMsgs, err = api.MessagesReadHistory(ctx, &tg.MessagesReadHistoryRequest{
|
||||
Peer: inputPeer,
|
||||
})
|
||||
|
|
@ -58,7 +56,6 @@ func (c *Client) ReadHistory(args ReadArguments) (*mcp.ToolResponse, error) {
|
|||
PtsCount: 1,
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unexpected input peer type: %T", p)
|
||||
}
|
||||
|
|
|
|||
5
main.go
5
main.go
|
|
@ -59,6 +59,11 @@ func main() {
|
|||
Value: sesionPath,
|
||||
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{
|
||||
Name: "dry",
|
||||
Usage: "Test configuration",
|
||||
|
|
|
|||
48
serve.go
48
serve.go
|
|
@ -7,6 +7,7 @@ import (
|
|||
"os"
|
||||
|
||||
"github.com/chaindead/telegram-mcp/internal/tg"
|
||||
"github.com/invopop/jsonschema"
|
||||
|
||||
mcp "github.com/metoro-io/mcp-golang"
|
||||
"github.com/metoro-io/mcp-golang/transport/stdio"
|
||||
|
|
@ -20,6 +21,10 @@ func serve(ctx context.Context, cmd *cli.Command) error {
|
|||
sessionPath := cmd.String("session")
|
||||
dryRun := cmd.Bool("dry")
|
||||
|
||||
if schemaURL := cmd.String("schema-version"); schemaURL != "" {
|
||||
jsonschema.Version = schemaURL
|
||||
}
|
||||
|
||||
_, err := os.Stat(sessionPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("session file not found(%s): %w", sessionPath, err)
|
||||
|
|
@ -50,24 +55,35 @@ func serve(ctx context.Context, cmd *cli.Command) error {
|
|||
|
||||
answer, err = client.GetHistory(tg.HistoryArguments{Name: os.Getenv("TG_TEST_USERNAME")})
|
||||
if err != nil {
|
||||
return fmt.Errorf("get histore: %w", err)
|
||||
return fmt.Errorf("get nickname history: %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")
|
||||
|
||||
answer, err = client.SendDraft(tg.DraftArguments{Name: os.Getenv("TG_TEST_USERNAME"), Text: "test draft"})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Check SendDraft: FAIL")
|
||||
} else {
|
||||
log.Info().RawJSON("answer", []byte(answer.Content[0].TextContent.Text)).Msg("Check SendDraft: 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"})
|
||||
// if err != nil {
|
||||
// log.Err(err).Msg("Check SendDraft: FAIL")
|
||||
// } else {
|
||||
// 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")})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Check ReadHistory: FAIL")
|
||||
} else {
|
||||
log.Info().RawJSON("answer", []byte(answer.Content[0].TextContent.Text)).Msg("Check ReadHistory: OK")
|
||||
}
|
||||
// answer, err = client.ReadHistory(tg.ReadArguments{Name: os.Getenv("TG_TEST_USERNAME")})
|
||||
// if err != nil {
|
||||
// log.Err(err).Msg("Check ReadHistory: FAIL")
|
||||
// } else {
|
||||
// log.Info().RawJSON("answer", []byte(answer.Content[0].TextContent.Text)).Msg("Check ReadHistory: OK")
|
||||
// }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -82,17 +98,17 @@ func serve(ctx context.Context, cmd *cli.Command) error {
|
|||
return fmt.Errorf("register dialogs tool: %w", err)
|
||||
}
|
||||
|
||||
err = server.RegisterTool("tg_dialog", "Get messages of telegram dialog (channel, user)", client.GetHistory)
|
||||
err = server.RegisterTool("tg_dialog", "Get messages of telegram dialog", client.GetHistory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("register dialogs tool: %w", err)
|
||||
}
|
||||
|
||||
err = server.RegisterTool("tg_send", "Send draft message to dialog (channel, user)", client.SendDraft)
|
||||
err = server.RegisterTool("tg_send", "Send draft message to dialog", client.SendDraft)
|
||||
if err != nil {
|
||||
return fmt.Errorf("register dialogs tool: %w", err)
|
||||
}
|
||||
|
||||
err = server.RegisterTool("tg_read", "Mark dialog messages as read (channel, user)", client.ReadHistory)
|
||||
err = server.RegisterTool("tg_read", "Mark dialog messages as read", client.ReadHistory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("register read tool: %w", err)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue