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 SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ running — check `tgcli backfill status`).
```bash
tgcli contacts search "alex"
tgcli groups list --query "Nha Trang"
tgcli groups members list --chat @mygroup # list all members
tgcli groups members list --chat @mygroup --search "alex" # filter by name/username
```

## Output Formats
Expand Down
32 changes: 32 additions & 0 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,13 @@ function buildProgram() {
.option('--chat <id|username>', 'Group identifier')
.option('--user <id>', 'User id', collectList)
.action(withGlobalOptions((globalFlags, options) => runGroupMembersRemove(globalFlags, options)));
groupMembers
.command('list')
.description('List group members')
.option('--chat <id|username>', 'Group identifier')
.option('--limit <n>', 'Max members to fetch')
.option('--search <text>', 'Filter members by name or username')
.action(withGlobalOptions((globalFlags, options) => runGroupMembersList(globalFlags, options)));
const groupInvite = groups.command('invite').description('Manage invite links');
groupInvite
.command('get')
Expand Down Expand Up @@ -3184,6 +3191,31 @@ async function runGroupMembersRemove(globalFlags, options = {}) {
}
}

async function runGroupMembersList(globalFlags, options = {}) {
if (!options.chat) {
throw new Error('--chat is required');
}
const members = await runOperation(globalFlags, {
op: 'groupMembersList',
args: {
chat: options.chat,
limit: parsePositiveInt(options.limit, '--limit'),
search: options.search,
},
});
if (globalFlags.json) {
writeJson(members);
} else {
for (const member of members) {
const handle = member.username ? `@${member.username}` : member.userId;
const flags = [member.status, member.isBot ? 'bot' : null]
.filter(Boolean)
.join(', ');
console.log(`${member.name}\t${handle}\t${member.userId}${flags ? `\t(${flags})` : ''}`);
}
}
}

async function runGroupInviteLinkGet(globalFlags, options = {}) {
if (!options.chat) {
throw new Error('--chat is required');
Expand Down
9 changes: 9 additions & 0 deletions core/operations.js
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,14 @@ async function groupMembersRemove(ctx, args = {}) {
return { channelId: args.chat, ...result };
}

// args: { chat, limit?, search? }. Lists group members.
async function groupMembersList(ctx, args = {}) {
return ctx.telegramClient.getGroupMembers(args.chat, {
limit: args.limit,
search: args.search,
});
}

// args: { chat }. Returns the primary invite-link descriptor.
async function getGroupInviteLink(ctx, args = {}) {
return ctx.telegramClient.getGroupInviteLink(args.chat);
Expand Down Expand Up @@ -967,6 +975,7 @@ export const OPERATIONS = {
groupsRename,
groupMembersAdd,
groupMembersRemove,
groupMembersList,
getGroupInviteLink,
revokeGroupInviteLink,
groupsJoin,
Expand Down
1 change: 1 addition & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ Legacy `--offset-id` is accepted as a hidden alias for `--before-id`.
- groups rename --chat <id> --name "New Name"
- groups members add --chat <id> --user <id> [--user ...]
- groups members remove --chat <id> --user <id> [--user ...]
- groups members list --chat <id> [--limit <n>] [--search <text>]
- groups invite get --chat <id>
- groups invite revoke --chat <id>
- groups join --code <invite-code>
Expand Down
4 changes: 4 additions & 0 deletions docs/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ This document defines the consolidated MCP tool surface. The goal is fewer tools
- Params: channelId (required), userIds (array, required)
- Output: { channelId, removed, failed }.

### groupsMembersList
- Params: channelId (required), limit (optional, default 200), search (optional)
- Output: array of { userId, username, name, isBot, status }.

### groupsInviteLinkGet
- Params: channelId (required)
- Output: invite link metadata.
Expand Down
37 changes: 37 additions & 0 deletions mcp-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,20 @@ const groupsMembersRemoveSchema = {
.describe("User IDs or usernames to remove"),
};

const groupsMembersListSchema = {
channelId: channelIdSchema.describe("Group ID or username"),
limit: z
.number({ invalid_type_error: "limit must be a number" })
.int()
.positive()
.optional()
.describe("Maximum number of members to fetch (default 200)"),
search: z
.string({ invalid_type_error: "search must be a string" })
.optional()
.describe("Filter members by name or username"),
};

const groupsInviteLinkGetSchema = {
channelId: channelIdSchema.describe("Group ID or username"),
};
Expand Down Expand Up @@ -1351,6 +1365,29 @@ function createServerInstance() {
},
);

server.tool(
"groupsMembersList",
"Lists members of a group, optionally filtered by name or username.",
groupsMembersListSchema,
async ({ channelId, limit, search }) => {
await ensureTelegramConnected();
const members = await OPERATIONS.groupMembersList(warmServices, {
chat: channelId,
limit,
search,
});

return {
content: [
{
type: "text",
text: JSON.stringify(members, null, 2),
},
],
};
},
);

server.tool(
"groupsInviteLinkGet",
"Gets the primary invite link for a group.",
Expand Down
34 changes: 34 additions & 0 deletions telegram-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -1367,6 +1367,40 @@ class TelegramClient {
return { removed, failed };
}

async getGroupMembers(channelId, options = {}) {
await this.ensureLogin();
const peerRef = normalizeChannelId(channelId);
const limit = options.limit ?? 200;
const query = (options.search ?? options.query ?? '').trim();
const needle = query.toLowerCase();
const members = [];
// Pass the query server-side when supported; also filter client-side so the
// search works reliably regardless of backend participant-search behaviour.
for await (const member of this.client.iterChatMembers(peerRef, {
query: query || undefined,
})) {
const user = member.user ?? {};
const name =
user.displayName ||
[user.firstName, user.lastName].filter(Boolean).join(' ') ||
'Unknown';
const entry = {
userId: user.id?.toString?.() ?? null,
username: user.username ?? null,
name,
isBot: typeof user.isBot === 'boolean' ? user.isBot : null,
status: typeof member.status === 'string' ? member.status : null,
};
if (needle) {
const haystack = `${entry.name} ${entry.username ?? ''}`.toLowerCase();
if (!haystack.includes(needle)) continue;
}
members.push(entry);
if (members.length >= limit) break;
}
return members;
}

async getGroupInviteLink(channelId) {
await this.ensureLogin();
const peerRef = normalizeChannelId(channelId);
Expand Down