mirror of
https://github.com/anthropics/claude-plugins-official.git
synced 2026-05-02 08:02:42 +00:00
Compare commits
1 Commits
tobin/mcp-
...
daisy/chan
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10b57b54a1 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "discord",
|
"name": "discord",
|
||||||
"description": "Discord channel for Claude Code \u2014 messaging bridge with built-in access control. Manage pairing, allowlists, and policy via /discord:access.",
|
"description": "Discord channel for Claude Code \u2014 messaging bridge with built-in access control. Manage pairing, allowlists, and policy via /discord:access.",
|
||||||
"version": "0.0.2",
|
"version": "0.0.3",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"discord",
|
"discord",
|
||||||
"messaging",
|
"messaging",
|
||||||
|
|||||||
@@ -22,8 +22,12 @@ import {
|
|||||||
GatewayIntentBits,
|
GatewayIntentBits,
|
||||||
Partials,
|
Partials,
|
||||||
ChannelType,
|
ChannelType,
|
||||||
|
ButtonBuilder,
|
||||||
|
ButtonStyle,
|
||||||
|
ActionRowBuilder,
|
||||||
type Message,
|
type Message,
|
||||||
type Attachment,
|
type Attachment,
|
||||||
|
type Interaction,
|
||||||
} from 'discord.js'
|
} from 'discord.js'
|
||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
import { readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync, statSync, renameSync, realpathSync, chmodSync } from 'fs'
|
import { readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync, statSync, renameSync, realpathSync, chmodSync } from 'fs'
|
||||||
@@ -479,13 +483,24 @@ mcp.setNotificationHandler(
|
|||||||
const text =
|
const text =
|
||||||
`🔐 Permission request [${request_id}]\n` +
|
`🔐 Permission request [${request_id}]\n` +
|
||||||
`${tool_name}: ${description}\n` +
|
`${tool_name}: ${description}\n` +
|
||||||
`${input_preview}\n\n` +
|
`${input_preview}`
|
||||||
`Reply "yes ${request_id}" to allow or "no ${request_id}" to deny.`
|
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId(`perm:allow:${request_id}`)
|
||||||
|
.setLabel('Allow')
|
||||||
|
.setEmoji('✅')
|
||||||
|
.setStyle(ButtonStyle.Success),
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId(`perm:deny:${request_id}`)
|
||||||
|
.setLabel('Deny')
|
||||||
|
.setEmoji('❌')
|
||||||
|
.setStyle(ButtonStyle.Danger),
|
||||||
|
)
|
||||||
for (const userId of access.allowFrom) {
|
for (const userId of access.allowFrom) {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
const user = await client.users.fetch(userId)
|
const user = await client.users.fetch(userId)
|
||||||
await user.send(text)
|
await user.send({ content: text, components: [row] })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
process.stderr.write(`permission_request send to ${userId} failed: ${e}\n`)
|
process.stderr.write(`permission_request send to ${userId} failed: ${e}\n`)
|
||||||
}
|
}
|
||||||
@@ -718,6 +733,31 @@ client.on('error', err => {
|
|||||||
process.stderr.write(`discord channel: client error: ${err}\n`)
|
process.stderr.write(`discord channel: client error: ${err}\n`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Button-click handler for permission requests. customId is
|
||||||
|
// `perm:allow:<id>` or `perm:deny:<id>` — set when the request was sent.
|
||||||
|
// Security mirrors the text-reply path: allowFrom must contain the sender.
|
||||||
|
client.on('interactionCreate', async (interaction: Interaction) => {
|
||||||
|
if (!interaction.isButton()) return
|
||||||
|
const m = /^perm:(allow|deny):([a-km-z]{5})$/.exec(interaction.customId)
|
||||||
|
if (!m) return
|
||||||
|
const access = loadAccess()
|
||||||
|
if (!access.allowFrom.includes(interaction.user.id)) {
|
||||||
|
await interaction.reply({ content: 'Not authorized.', ephemeral: true }).catch(() => {})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const [, behavior, request_id] = m
|
||||||
|
void mcp.notification({
|
||||||
|
method: 'notifications/claude/channel/permission',
|
||||||
|
params: { request_id, behavior },
|
||||||
|
})
|
||||||
|
const label = behavior === 'allow' ? '✅ Allowed' : '❌ Denied'
|
||||||
|
// Replace buttons with the outcome so the same request can't be answered
|
||||||
|
// twice and the chat history shows what was chosen.
|
||||||
|
await interaction
|
||||||
|
.update({ content: `${interaction.message.content}\n\n${label}`, components: [] })
|
||||||
|
.catch(() => {})
|
||||||
|
})
|
||||||
|
|
||||||
client.on('messageCreate', msg => {
|
client.on('messageCreate', msg => {
|
||||||
if (msg.author.bot) return
|
if (msg.author.bot) return
|
||||||
handleInbound(msg).catch(e => process.stderr.write(`discord: handleInbound failed: ${e}\n`))
|
handleInbound(msg).catch(e => process.stderr.write(`discord: handleInbound failed: ${e}\n`))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "telegram",
|
"name": "telegram",
|
||||||
"description": "Telegram channel for Claude Code \u2014 messaging bridge with built-in access control. Manage pairing, allowlists, and policy via /telegram:access.",
|
"description": "Telegram channel for Claude Code \u2014 messaging bridge with built-in access control. Manage pairing, allowlists, and policy via /telegram:access.",
|
||||||
"version": "0.0.2",
|
"version": "0.0.3",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"telegram",
|
"telegram",
|
||||||
"messaging",
|
"messaging",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
CallToolRequestSchema,
|
CallToolRequestSchema,
|
||||||
} from '@modelcontextprotocol/sdk/types.js'
|
} from '@modelcontextprotocol/sdk/types.js'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { Bot, GrammyError, InputFile, type Context } from 'grammy'
|
import { Bot, GrammyError, InlineKeyboard, InputFile, type Context } from 'grammy'
|
||||||
import type { ReactionTypeEmoji } from 'grammy/types'
|
import type { ReactionTypeEmoji } from 'grammy/types'
|
||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
import { readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync, statSync, renameSync, realpathSync, chmodSync } from 'fs'
|
import { readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync, statSync, renameSync, realpathSync, chmodSync } from 'fs'
|
||||||
@@ -399,10 +399,12 @@ mcp.setNotificationHandler(
|
|||||||
const text =
|
const text =
|
||||||
`🔐 Permission request [${request_id}]\n` +
|
`🔐 Permission request [${request_id}]\n` +
|
||||||
`${tool_name}: ${description}\n` +
|
`${tool_name}: ${description}\n` +
|
||||||
`${input_preview}\n\n` +
|
`${input_preview}`
|
||||||
`Reply "yes ${request_id}" to allow or "no ${request_id}" to deny.`
|
const keyboard = new InlineKeyboard()
|
||||||
|
.text('✅ Allow', `perm:allow:${request_id}`)
|
||||||
|
.text('❌ Deny', `perm:deny:${request_id}`)
|
||||||
for (const chat_id of access.allowFrom) {
|
for (const chat_id of access.allowFrom) {
|
||||||
void bot.api.sendMessage(chat_id, text).catch(e => {
|
void bot.api.sendMessage(chat_id, text, { reply_markup: keyboard }).catch(e => {
|
||||||
process.stderr.write(`permission_request send to ${chat_id} failed: ${e}\n`)
|
process.stderr.write(`permission_request send to ${chat_id} failed: ${e}\n`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -683,6 +685,37 @@ bot.command('status', async ctx => {
|
|||||||
await ctx.reply(`Not paired. Send me a message to get a pairing code.`)
|
await ctx.reply(`Not paired. Send me a message to get a pairing code.`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Inline-button handler for permission requests. Callback data is
|
||||||
|
// `perm:allow:<id>` or `perm:deny:<id>` — set when the request was sent.
|
||||||
|
// Security mirrors the text-reply path: allowFrom must contain the sender.
|
||||||
|
bot.on('callback_query:data', async ctx => {
|
||||||
|
const data = ctx.callbackQuery.data
|
||||||
|
const m = /^perm:(allow|deny):([a-km-z]{5})$/.exec(data)
|
||||||
|
if (!m) {
|
||||||
|
await ctx.answerCallbackQuery().catch(() => {})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const access = loadAccess()
|
||||||
|
const senderId = String(ctx.from.id)
|
||||||
|
if (!access.allowFrom.includes(senderId)) {
|
||||||
|
await ctx.answerCallbackQuery({ text: 'Not authorized.' }).catch(() => {})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const [, behavior, request_id] = m
|
||||||
|
void mcp.notification({
|
||||||
|
method: 'notifications/claude/channel/permission',
|
||||||
|
params: { request_id, behavior },
|
||||||
|
})
|
||||||
|
const label = behavior === 'allow' ? '✅ Allowed' : '❌ Denied'
|
||||||
|
await ctx.answerCallbackQuery({ text: label }).catch(() => {})
|
||||||
|
// Replace buttons with the outcome so the same request can't be answered
|
||||||
|
// twice and the chat history shows what was chosen.
|
||||||
|
const msg = ctx.callbackQuery.message
|
||||||
|
if (msg && 'text' in msg && msg.text) {
|
||||||
|
await ctx.editMessageText(`${msg.text}\n\n${label}`).catch(() => {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
bot.on('message:text', async ctx => {
|
bot.on('message:text', async ctx => {
|
||||||
await handleInbound(ctx, ctx.message.text, undefined)
|
await handleInbound(ctx, ctx.message.text, undefined)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user