Skip to main content
Glama

tweetly

CI License: MIT Stack Status

Self-hosted automation layer for X (Twitter), built around the Model Context Protocol. Bring your own AI agent — Claude Code, Cursor, Codex, anything that speaks MCP — and tweetly executes the post / engage / read actions on X through a real browser session, not the public API.

Live demo: tw-panel.beydemir.dev — request a magic link with your email, mint a tk_* API key, plug it into your MCP client. For production, self-host on your own infrastructure — see Deploy below.


Table of contents


Disclaimer

tweetly does not use X's official public API. It drives X through a real browser session (Patchright + persisted cookies). Two consequences follow:

  1. It may violate X's Terms of Service. Automation, third-party session sharing, and synthetic engagement all sit in ToS gray/red territory. Account suspension risk is on you.

  2. This is not a vetted enterprise product. The repository is a research / personal-use project. Run a risk assessment before pointing it at customer accounts in production.

All liability remains with the user under the MIT License — see LICENSE.


Project status

Public beta. The auth, action engine, MCP, and REST surfaces are stable; breaking changes are documented in CHANGELOG.md. Coverage: 403 unit tests + 24 integration tests on every push.

MCP clients verified against this build:

  • Claude Code (CLI)

  • Claude Desktop

  • Cursor (via MCP HTTP transport)

  • ChatGPT custom GPT actions (REST)

  • Generic MCP clients via /mcp/sse

Known weak spots are tracked as labeled issues — see open issues and especially security and good first issue for current priorities.


Quick start

Get a local dev instance posting noop actions in under five minutes.

# 1. Clone and install
git clone https://github.com/beydemirfurkan/tweetly.git
cd tweetly
npm install
npm --prefix backend install --legacy-peer-deps
npm --prefix frontend install
npx patchright install chromium

# 2. Generate a 32-byte master key, then paste it into ENCRYPTION_KEY in .env
cp .env.example .env
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

# 3. Start postgres and apply migrations
docker compose up -d postgres
npm run db:migrate

# 4. Run backend + frontend
npm run dev:backend   # http://localhost:3001
npm run dev:frontend  # http://localhost:3000

Open http://localhost:3000, request a magic link with your email (the link is logged to backend stdout in dev), and mint a tk_* API key from the panel.

For real X delivery, set X_EXECUTOR_MODE=patchright and connect an X account; for dry runs, leave it as noop. See Connect an X account for the full server-side login flow.


Architecture

Stack: NestJS 11, TypeScript, PostgreSQL + TypeORM, Patchright (anti-detection browser), Model Context Protocol SDK, Next.js 16, React 19, Tailwind v4.

High-level flow

flowchart LR
    Client["AI agent<br/>(Claude Code, Cursor, ...)"]
    Panel["Panel<br/>(Next.js 16)"]

    subgraph Backend["NestJS backend"]
        direction TB
        Guard["ApiKeyGuard<br/>(tk_* user keys)"]
        REST["REST · /api/v1/*"]
        MCP["MCP · /mcp/sse"]
        Engine["Action engine<br/>(claim · retry · circuit breaker)"]
        Browser["XBrowserService<br/>+ executors (Patchright)"]
    end

    DB[("PostgreSQL<br/>(actions, accounts,<br/>monitors, jobs)")]
    X[("X · twitter.com")]
    Webhook["Tenant webhook<br/>receivers"]

    Client -- "MCP / Bearer tk_*" --> Guard
    Panel -- "REST / Bearer tk_*" --> Guard
    Guard --> REST
    Guard --> MCP
    REST --> Engine
    MCP --> Engine
    Engine --> Browser
    Engine <--> DB
    Browser --> X
    Engine -- "monitor events" --> Webhook

Post-tweet sequence

sequenceDiagram
    autonumber
    participant Agent as AI agent
    participant API as Backend API
    participant Q as Action queue (Postgres)
    participant W as ClaimWorker
    participant X as Patchright → X

    Agent->>API: post_tweet(text, account)<br/>Authorization: Bearer tk_*
    API->>API: ApiKeyGuard verifies key<br/>resolves userId, accountId
    API->>Q: INSERT post_actions (pending)
    API-->>Agent: 202 { id, idempotencyKey }
    loop every WORKER_POLL_MS
        W->>Q: SELECT FOR UPDATE SKIP LOCKED
        Q-->>W: claim row → status=claimed
        W->>X: launch context, navigate, post
        alt success
            X-->>W: tweet URL
            W->>Q: status=succeeded
        else transient failure
            W->>Q: status=pending (retry, backoff)
        else permanent failure
            W->>Q: status=dead
        end
    end

Project layout

backend/src/
  accounts/          Account management (per-user X session tokens)
  action-engine/     ClaimWorker, ExecutorRegistry, CircuitBreaker, RetryPolicy
                     GenericActionRepository (FOR UPDATE SKIP LOCKED)
  admin-api/         AdminApiController, AdminTokenGuard, AdminApiService
  ai-copilot/        Optional content analysis (env-gated to specific emails)
  auth/              UsersService, ApiKeyService, MagicLinkService, ApiKeyGuard
  content-memory/    Jaccard similarity dedup (optional)
  domain/            Port interfaces, domain services, action types
  mcp/               MCP server (SSE transport, ~43 tools)
  monitoring/        Account monitor + webhook delivery
  oauth/             OAuth2 authorization server for MCP clients
  observability/     HealthController, MetricsController (Prometheus)
  persistence/       TypeORM DataSource, entities, migrations
  public-api/        REST controllers under /api/v1, user-scoped
  settings/          Per-account override-aware settings service
  x-automation/      XBrowserService, XPostFlowService, SelectorRegistry
                     NoOp + Patchright executors per action type

frontend/src/
  app/[locale]/      Next.js 16 panel (i18n: tr/en)
  components/        Shadcn-based UI
  i18n/              next-intl config
  lib/               API client, auth context, hooks

Action state machine

pending → claimed → running → succeeded
                           ↘ failed → pending (retry)
                                    ↘ dead
         ↘ cancelled (admin)

Every action type (post, reply, like, bookmark, retweet, quote, follow, unlike, unretweet, unfollow, delete_tweet, dm, profile_update, avatar_update, banner_update) lives in its own Postgres table for predictable indexes, idempotency keys, and per-type metrics.


MCP tool surface

Same Zod schemas back the MCP and REST surfaces, so a tool name in MCP maps 1:1 to a route in REST.

Write (queue-backed): post_tweet · reply_to_tweet · like_tweet · retweet_tweet · quote_tweet · bookmark_tweet · follow_account · post_thread · unlike_tweet · unretweet_tweet · unfollow_account · delete_tweet · send_dm · update_profile · update_avatar · update_banner

Read (synchronous): search_tweets · get_user · get_tweet · get_user_tweets · search_users · get_user_followers · get_user_following · get_tweet_retweeters · get_tweet_quotes · get_tweet_replies · get_user_mentions · get_x_trending · get_user_likes · get_my_bookmarks · get_thread · get_mutual_followers · get_user_lists · get_list · get_list_members · get_list_subscribers

Bulk extractions (async, file output): create_extraction · get_extraction · list_extractions · cancel_extraction

Management: get_accounts · get_account_health · connect_x_account · reauth_x_account · get_x_login_job · list_actions · cancel_action · replay_action · get_settings · update_settings

Monitor: create_monitor · list_monitors · get_monitor · rotate_secret · delete_monitor · pause_monitor

Breaking (2026-05-03): retweetretweet_tweet, unretweetunretweet_tweet. Old names now return Unknown tool.


Setup

git clone https://github.com/beydemirfurkan/tweetly.git
cd tweetly
npm install
npm --prefix backend install --legacy-peer-deps
npm --prefix frontend install
npx patchright install chromium

cp .env.example .env
# Generate a 32-byte master key and paste it as ENCRYPTION_KEY in .env:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

docker compose up -d postgres
npm run db:migrate

Commands

npm run build          # tsc → dist/ for backend; next build for frontend
npm run dev:backend    # backend dev server (http://localhost:3001)
npm run dev:frontend   # frontend dev server (http://localhost:3000)
npm test               # backend unit tests + frontend tests
npm run lint           # backend + frontend lint
npm run typecheck      # backend + frontend type-check

npm run db:migrate                            # apply pending migrations
npm run db:migrate:revert                     # revert the last migration
npm --prefix backend run db:migrate:generate -- <Name>  # diff entities → src/persistence/migrations/<ts>-<Name>.ts

Smoke tests

Run the MCP and REST tool matrix against a local backend before deploying:

cd backend
TWEETLY_API_KEY=tk_... TWEETLY_ACCOUNT_ID=your-x-handle npm run smoke:mcp
TWEETLY_API_KEY=tk_... TWEETLY_ACCOUNT_ID=your-x-handle npm run smoke:rest

# Include X read paths
TWEETLY_API_KEY=tk_... TWEETLY_ACCOUNT_ID=... TWEETLY_SMOKE_SUITE=read npm run smoke:mcp
TWEETLY_API_KEY=tk_... TWEETLY_ACCOUNT_ID=... TWEETLY_SMOKE_SUITE=read npm run smoke:rest

# Queue/write tools require explicit opt-in
TWEETLY_API_KEY=tk_... TWEETLY_ACCOUNT_ID=... TWEETLY_SMOKE_SUITE=queue \
  TWEETLY_ALLOW_WRITE_SMOKE=true \
  TWEETLY_TARGET_TWEET_URL=https://x.com/.../status/... \
  npm run smoke:mcp

The destructive suite (delete_tweet, update_profile, send_dm, unfollow, ...) only runs against a throwaway test account with TWEETLY_ALLOW_DESTRUCTIVE_SMOKE=true.


Environment variables

Variable

Required

Description

DATABASE_URL

Yes

PostgreSQL connection URL

ENCRYPTION_KEY

Yes

32-byte base64/hex master key for AES-256-GCM encryption of login credentials AND X session cookies (auth_token, ct0, auth_multi, twid). See "Rotating ENCRYPTION_KEY" below

X_EXECUTOR_MODE

Yes

patchright for real X delivery; noop for local dry-runs

BOOTSTRAP_ADMIN_TOKEN

First boot

Temporary token used to seed secrets.admin_token in the DB

BOOTSTRAP_ADMIN_EMAIL

First boot

Email of the first admin user to create

CORS_ORIGINS

Production

Comma-separated origin allowlist (empty rejects all)

REDIS_URL

Multi-instance

Required when running 2+ backend replicas

AI_COPILOT_ADMIN_EMAILS

Optional

Comma-separated emails authorized for the AI Copilot module (empty disables)

TRUST_PROXY

Optional

app.set('trust proxy', ...) value for IP-based rate limit accuracy behind a reverse proxy

After first boot, write a permanent admin token to the DB and remove BOOTSTRAP_ADMIN_TOKEN:

curl -X PUT -H "Authorization: Bearer $BOOTSTRAP_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"adminToken":"<another-random-32-byte-hex>"}' \
  http://localhost:3001/admin/secrets

SMTP credentials live in the database

Magic-link emails ship through SMTP. No SMTP variables are read from env — credentials live in the DB via PUT /admin/secrets. Pick a provider (Postmark, Mailgun, SES, Gmail, ...) and write the credentials in:

curl -X PUT -H "Authorization: Bearer $ADMIN_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "mailProvider": "smtp",
    "smtpHost": "smtp.postmarkapp.com",
    "smtpPort": 587,
    "smtpUser": "your-server-token",
    "smtpPass": "your-server-token",
    "smtpSecure": false,
    "mailFrom": "tweetly <noreply@yourdomain.com>"
  }' \
  http://localhost:3001/admin/secrets

The transporter rebuilds on the next magic-link send — no restart needed. Updating credentials at the same endpoint cycles the previous transporter automatically. If mailProvider stays console (the default), magic links are written to backend stdout — ideal for local dev.


Connect an X account

tweetly logs into X through its own browser automation; users never copy auth_token / ct0 / twid. Kick off a server-side login job:

curl -X POST -H "Authorization: Bearer $TWEETLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"username":"foo","password":"x-password","email":"foo@example.com","totpSecret":null,"saveTotpSecret":false,"proxyCountry":"TR"}' \
  http://localhost:3001/api/v1/accounts/connect

The response is 202 Accepted with a jobId. Poll its status:

curl -H "Authorization: Bearer $TWEETLY_API_KEY" \
  http://localhost:3001/api/v1/accounts/login-jobs/$JOB_ID

When a session breaks, re-authenticate the same account:

curl -X POST -H "Authorization: Bearer $TWEETLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"password":"x-password","email":"foo@example.com","totpSecret":null,"saveTotpSecret":false,"proxyCountry":"TR"}' \
  http://localhost:3001/api/v1/accounts/foo/reauth

proxyCountry is optional. If omitted, the backend uses LOGIN_DEFAULT_PROXY_COUNTRY; for reauth, it falls back to the account's stored proxy_country. X temporarily blocks bursts of logins from the same server IP, so configuring per-region egress proxies in production is recommended:

LOGIN_DEFAULT_PROXY_COUNTRY=TR
LOGIN_FALLBACK_PROXY_COUNTRIES=US,DE
LOGIN_PROXY_TR=http://user:pass@tr.proxy.example:8080
LOGIN_PROXY_US=http://user:pass@us.proxy.example:8080

If the X onboarding flow returns a transient "try again later" or stalls on the username step, the worker retries once with the first configured fallback proxy country.

Session expiry semantics

  • 1+ consecutive auth failures — an "Expired token?" badge appears on the Accounts list (hover for the last error reason).

  • 3 consecutive auth failures — the account is auto-paused. Queued actions are held; production stalls until you re-authenticate from the panel.


API key onboarding

The panel is the canonical place to mint and manage tk_* keys.

  1. Request a magic link. Visit http://localhost:3000 (or your panel domain), enter your email. In dev with mailProvider=console, copy the link from backend logs. In prod, click the link in the email.

  2. Verify the link. Hitting the URL opens /auth/verify, which exchanges the token for a panel session.

  3. Mint a key. Open the API Keys page, click Create, give it a label, copy the tk_* value — it is shown once.

  4. Use the key. Send Authorization: Bearer tk_... to any /api/v1/* endpoint or the /mcp/sse connection.

  5. Revoke when needed. The panel lists every key with last-used timestamp; revoke compromises immediately.

Auth model summary

  • tk_* user keys → /mcp/*, /api/v1/* (the user's own accounts, multi-tenant)

  • secrets.admin_token/admin/* (operator/sysadmin endpoints, all users)

Never hand the admin token to an MCP client.


MCP connection

# After minting a tk_* key from the panel:
claude mcp add tweetly --url http://localhost:3001/mcp/sse \
  --header "Authorization: Bearer $TWEETLY_API_KEY"

Then, inside your agent: "Post 'hello world' through tweetly" triggers post_tweet, the action engine enqueues it, and Patchright publishes on X.


Webhook HMAC verification

When you create a monitor, the response includes webhookSecret — shown once. Your webhook receiver must verify the X-Tweetly-Signature header:

// Express example
app.post('/tweetly-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.header('X-Tweetly-Signature') ?? '';
  const [tPart, vPart] = header.split(',');
  const ts = tPart?.split('=')[1];
  const sig = vPart?.split('=')[1];
  if (!ts || !sig) return res.status(400).end();

  const expected = crypto
    .createHmac('sha256', process.env.TWEETLY_WEBHOOK_SECRET)
    .update(`${ts}.${req.body.toString('utf8')}`)
    .digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.status(401).end();
  }
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.status(401).end();

  const payload = JSON.parse(req.body.toString('utf8'));
  // ...
  res.status(200).end();
});

Lost the secret? Rotate via POST /api/v1/monitors/:id/rotate-secret.


Admin API

# Public
curl http://localhost:3001/health
curl http://localhost:3001/ready

# Status / metrics (admin token required)
curl -H "Authorization: Bearer $ADMIN_API_TOKEN" http://localhost:3001/admin/status
curl -H "Authorization: Bearer $ADMIN_API_TOKEN" http://localhost:3001/metrics
curl -H "Authorization: Bearer $ADMIN_API_TOKEN" http://localhost:3001/admin/queue/depth

# Action management
curl -H "Authorization: Bearer $ADMIN_API_TOKEN" "http://localhost:3001/admin/actions?type=post&status=dead"
curl -X POST -H "Authorization: Bearer $ADMIN_API_TOKEN" http://localhost:3001/admin/actions/post/UUID/replay
curl -X POST -H "Authorization: Bearer $ADMIN_API_TOKEN" http://localhost:3001/admin/actions/post/UUID/cancel

Rotating ENCRYPTION_KEY

The same ENCRYPTION_KEY protects (a) login-job passwords + TOTP secrets and (b) X session cookies (auth_token, ct0, auth_multi, twid). All ciphertext is stamped with a v1: version prefix.

A naive key swap invalidates every stored credential. For zero-downtime rotation:

  1. Generate the new key (keep the old one):

    node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
  2. Bump cipher version in code: edit backend/src/common/crypto/credential-cipher.service.ts to introduce a v2: envelope using the new key, while keeping the v1: decrypt path mapped to the old key (parallel-decrypt window). Cookies/credentials written from this point use v2:; existing v1: payloads continue to decrypt with the old key.

  3. Deploy, run for a migration window (24–72h is typical).

  4. One-shot re-encrypt existing v1: rows under the new key:

    COOKIE_ENCRYPT_MIGRATE=true tsx backend/src/scripts/encrypt-account-cookies.ts

    (This script is also the path used when retro-fitting cookie encryption to a deployment that ran with plaintext cookies before the change landed.)

  5. Remove the old key + v1: decrypt path in a follow-up release.

If you skip steps 2–4, you must instead force every user through a reconnect (POST /api/v1/accounts/:id/login-jobs) — old cookies become unreadable and recordSessionFailure will quickly pause those accounts.


Trust proxy (TRUST_PROXY)

The backend's IP-based rate limits — /auth/request-link (5/min, anti-mail-bomb) and /oauth/register (10/hr, anti-DCR-spam) — rely on Express's req.ip being the real client. Behind a reverse proxy or load balancer, req.ip defaults to the proxy's loopback address and X-Forwarded-For is not trusted, so the throttler effectively rate-limits the proxy, not the caller.

Set TRUST_PROXY to tell Express which upstream hops to trust. The right value depends on the deployment shape:

Layout

TRUST_PROXY

Notes

Local dev, no proxy

unset (defaults to loopback)

safe default

nginx / Coolify, single host

loopback,linklocal,uniquelocal

trust the proxy on the same network

Cloudflare in front of origin

loopback,linklocal,uniquelocal

CF rewrites the IP into X-Forwarded-For; trust the immediate upstream

AWS ALB / Vercel / Fly.io

1

trust exactly one upstream hop (the platform load balancer)

Two-tier (CDN → ALB → app)

2

trust two hops

If you forget to set this, the symptom is X-Forwarded-For: 1.1.1.<random> letting an attacker bypass the magic-link limit (see issue #3 for the curl repro). The fix is one env var.


Deploy

Docker Compose

cp .env.example .env
docker compose up --build

Volume

Contents

tweetly_state

/data — sessions, media, logs

tweetly_pgdata

PostgreSQL data directory

Coolify

Service

Type

Notes

tweetly-backend

Application (Dockerfile)

backend/ directory, Dockerfile build, port 3000

tweetly-frontend

Application (Dockerfile)

frontend/ directory, build arg NEXT_PUBLIC_API_URL=https://api.your-domain.com

tweetly-postgres

Managed Postgres

Coolify add-on, 16-alpine, persistent volume

Backend env:

DATABASE_URL=postgres://tweetly:tweetly@<coolify-postgres>:5432/tweetly
NODE_ENV=production
X_EXECUTOR_MODE=patchright
APP_URL=https://panel.yourdomain.com
CORS_ORIGINS=https://panel.yourdomain.com
BOOTSTRAP_ADMIN_TOKEN=<random-32-byte-hex>      # one-time
BOOTSTRAP_ADMIN_EMAIL=you@yourdomain.com         # first user's email
# REDIS_URL=redis://<coolify-redis>:6379         # required for 2+ instances

Frontend build arg:

NEXT_PUBLIC_API_URL=https://api.your-domain.com

If you keep the panel.*api.* naming convention, NEXT_PUBLIC_API_URL can be omitted — lib/api.ts derives it at runtime. Any other convention requires the build arg.

Persistent volume. The backend container mounts /data:

  • /data/user-data — X session profiles (cookie persistence)

  • /data/app-data/{errors,logs} — runtime artifacts

The Patchright Chromium binary lives at /app/browsers inside the image. Don't mount /data over /app/browsers — Coolify volume mounts shadow the in-image binary. In Coolify, set "Persistent Storage" → mount /data.

Bootstrap (one-time, post-deploy):

# 1. Create the first admin user
curl -X POST -H "Authorization: Bearer $BOOTSTRAP_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email":"you@yourdomain.com"}' \
  https://api.your-domain.com/admin/users

# 2. Write the permanent admin token + SMTP credentials
curl -X PUT -H "Authorization: Bearer $BOOTSTRAP_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "adminToken": "<another-random-32-byte-hex>",
    "mailProvider": "smtp",
    "smtpHost": "smtp.postmarkapp.com",
    "smtpPort": 587,
    "smtpUser": "<provider-user>",
    "smtpPass": "<provider-pass>",
    "mailFrom": "tweetly <noreply@yourdomain.com>"
  }' \
  https://api.your-domain.com/admin/secrets

# 3. Remove BOOTSTRAP_ADMIN_TOKEN from Coolify env, redeploy
# 4. Frontend → /login → enter email → click magic link → in

Migrations. From Coolify "Run Command":

npm run db:migrate

Run once after the first deploy. Subsequent migrations don't auto-apply on container start — run the command manually.


Multi-instance scaling

tweetly runs in a single Node process with zero extra configuration. To scale horizontally there are four coordination points; three are handled in code, the fourth is a load-balancer setting:

Component

Multi-instance setup

Action ClaimWorker

Postgres FOR UPDATE SKIP LOCKED already safe — no extra config

Rate limiter

Set REDIS_URL — shared counter, all instances count toward the same limit

Monitor poller

pg_try_advisory_lock leader election — only one instance polls per cycle

MCP SSE

Sticky session required at the load balancer (see below)

Sticky session. The MCP SSE connection is long-lived; the same user's /mcp/messages POSTs must land on the instance that opened the SSE stream. Hash-based sticky on Authorization or cookie-based affinity in Caddy / nginx / Traefik is enough; Coolify's "Session affinity" toggle does the same.

REDIS_URL. Required when running 2+ instances (rate limit + MCP session registry). Single-instance dev/prod falls back to in-memory.

Verify multi-instance. Bring up two instances and fire 31 PUT requests for the same user: the 30th and beyond must return 429 even when they hit the second instance (shared Redis counter). Without Redis, each instance has its own counter, so 60 requests would slip through. Monitor poller: only one instance logs Polling N monitor(s) (leader).


Observability

GET /metrics requires bearer auth (secrets.admin_token). Example Prometheus scrape:

scrape_configs:
  - job_name: tweetly
    metrics_path: /metrics
    static_configs:
      - targets: ['api.your-domain.com:443']
    scheme: https
    bearer_token: <secrets.admin_token>

Metric

Type

tweetly_action_total

Counter

tweetly_action_duration_ms

Histogram

tweetly_queue_depth

Gauge

tweetly_circuit_breaker_paused

Gauge

Grafana Cloud free tier? Grafana Agent or Alloy accepts the same config. See docs/12-queue-alarms.md for alert templates calibrated against the queue metrics.


Contributing

Issues and pull requests welcome. Read CONTRIBUTING.md for development setup and PR conventions, and SECURITY.md for the vulnerability-disclosure process.

The main branch is protected: pull requests require a passing CI run (backend and frontend jobs) and one approving review. Conventional Commits are documented in CONTRIBUTING.md.


Acknowledgments

Built on the shoulders of:

  • Patchright — anti-detection Playwright fork that does the heavy lifting against X's bot defenses.

  • Model Context Protocol SDK — the spec and reference server that makes MCP integration ergonomic.

  • NestJS — the backend framework that keeps the modular boundaries honest.

  • TypeORM — the data layer behind the action engine queue.

  • Next.js and Shadcn — the panel framework and component library.

  • nodemailer — magic-link delivery.

  • prom-client — Prometheus metrics export.


License

MIT © Furkan Beydemir.

Contributing · Security · Changelog · Issues · Releases

A
license - permissive license
-
quality - not tested
F
maintenance

Maintenance

Maintainers
18dResponse time
Release cycle
Releases (12mo)

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/beydemirfurkan/tweetly'

If you have feedback or need assistance with the MCP directory API, please join our Discord server