Marketing Ops
Reads repository data such as issues, pull requests, commits and releases to see what shipped, and compares public docs pages against the code they describe. Feeds blog/social draft generation, weekly newsletter content and docs-freshness flagging.
Sends and drafts email such as webinar invites and reminders, case-study permission asks, event follow-ups and the weekly newsletter.
Browser sign-in over your own OAuth client gives the server access to your Google Workspace apps (Docs, Sheets, Forms, Calendar and Gmail) without a per-app key.
Creates and manages calendar events for webinar scheduling and reminders.
Creates and updates Google Docs with generated content: blog drafts, social copy, case studies, insight memos and newsletter drafts.
Reads form responses, e.g. webinar registrations, survey answers and event lead scans, as the input to downstream workflows.
Writes and maintains spreadsheets such as competitor pricing history, prioritised SEO content gaps and tagged survey response tables.
Reads issues and shipped project data, and creates issues from SEO content gaps, site QA crawl findings and docs freshness problems.
Reads and writes Notion pages and databases for newsletter source material, enriched event leads, brand mention tracking and publishing blog posts and social snippet queues.
Posts alerts and updates to channels, e.g. competitor price changes, brand mentions, site QA issues, launch-day signup/bug pulses and insight memo summaries.
Reads customer and subscription data to identify longest-paying happy customers for case studies and to post hourly signup numbers during a launch.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Marketing Ops turn this week's shipped features into blog and social drafts"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Marketing Ops
Shipped work becomes blog drafts; competitor pricing, mentions and webinars run on schedule.
An MCP server with 13 workflows across Linear, GitHub, Google Docs, Slack, Firecrawl, Google Sheets, Tavily, Google Forms, Google Calendar, Gmail, Stripe, Granola and Notion. Each workflow is a prompt your agent runs as a slash command, over the 27 tools it needs and no others.
uv tool install https://github.com/r28ai/marketing-ops-mcp/releases/download/v0.1.0/marketing_ops_mcp-0.1.0-py3-none-any.whl
claude mcp add marketing -- marketing-ops-mcpIt installs with uv from this repository's release, with no git and nothing to build; nothing but Charter and the libraries it uses comes from PyPI. To update, run the install line from the latest release. If a desktop app cannot find marketing-ops-mcp, give it the full path from which marketing-ops-mcp (where marketing-ops-mcp on Windows).
Then ask your agent to connect your apps, or run /mcp__marketing__setup.
Connect your apps
Ask the agent to connect one ("connect Linear"). It tells you where to get that app's key and the command that stores it, and the next call works, with no restart. The agent never asks for a key in the chat.
Or connect everything this server uses from a terminal:
marketing-ops-mcp login # each app in turn
marketing-ops-mcp login linear # just one
marketing-ops-mcp status # what is connectedTokens and keys go to your operating system's keychain (macOS Keychain, Windows Credential Manager, the Secret Service on Linux), and are checked with one read-only call to the app's own API before they are kept. Every key, token and OAuth client is yours: we register no app with any of these services, and nothing passes through a server of ours, because there isn't one.
App | How it connects | Or set |
Linear | Your own key (get one), entered once. |
|
GitHub | Your own key (get one), entered once. |
|
Browser sign-in, over your own OAuth client (make one). |
| |
Slack | Your own key (get one), entered once. A bot token from your own Slack app, which the guide sets up in about three minutes. |
|
Firecrawl | Your own key (get one), entered once. |
|
Tavily | Your own key (get one), entered once. |
|
Stripe | Your own key (get one), entered once. |
|
Granola | Your own key (get one), entered once. In the Granola app: Settings → Connectors → API keys. Business plan or above. |
|
Notion | Your own key (get one), entered once. Then share the pages it should see with the integration. |
|
A variable set in your client's config always wins over the keychain.
Related MCP server: Freelancer Ops MCP
Workflows
Workflow | What you get | Apps |
What shipped → blog and social drafts | Marketing hears about features from the tracker, not from customers. | Linear, GitHub, Google Docs, Slack |
Competitor pricing tracker | Price and packaging changes, dated, in a sheet, with an alert when one moves. | Firecrawl, Google Sheets, Slack |
SEO content gap | Topics competitors rank for that you have no page on, as a prioritised backlog. | Firecrawl, Tavily, Google Sheets, Linear |
Webinar ops end to end | Registration, invite and reminders with no webinar tool. | Google Forms, Google Calendar, Gmail |
Customer story pipeline | Your longest-paying happy customers become case-study drafts and a permission ask. | Stripe, Granola, Google Docs, Gmail |
Weekly newsletter draft | Product news, posts and releases gathered into one draft every Friday. | Linear, GitHub, Notion, Google Docs, Gmail |
Event leads → enriched follow-ups | Booth scans become enriched leads with a follow-up waiting, same day. | Google Forms, Firecrawl, Notion, Gmail |
Brand mention monitor | Every mention worth replying to shows up with the context to reply. | Tavily, Firecrawl, Notion, Slack |
Webinar transcript → blog post | An hour of talk becomes a post and a queue of social snippets. | Granola, Google Docs, Notion |
Site QA crawl | Broken links, empty pages and stale pricing get issues before a prospect finds them. | Firecrawl, Linear, Slack |
Launch-day pulse | Hourly signups and fresh bugs posted to the launch channel without anyone refreshing dashboards. | Stripe, Linear, Slack |
Survey → insight memo | Raw responses become a tagged sheet and a one-page memo. | Google Forms, Google Sheets, Google Docs, Slack |
Docs site freshness check | Public docs pages older than the code they describe get flagged. | Firecrawl, GitHub, Linear |
Every prompt takes one optional argument, details: the repo, team, channel, customer or date range you mean, so the agent does not have to ask. In Claude Code, put it in quotes, or only its first word arrives:
/mcp__marketing__what_shipped_to_blog_and_social_drafts "everything shipped since the 1st"Reads run without asking. Before anything that creates, sends, changes or deletes, the prompt tells the agent to show you the call and wait.
4 of the 13 workflows need no Google or Granola credential.
Other clients
Claude Desktop: install uv if you have not, since Claude Desktop starts the server with it, then open the .mcpb from the latest release. Claude asks for any keys in its own settings and keeps them in your keychain. The first start takes a few seconds longer, while uv installs it.
VS Code (.vscode/mcp.json): VS Code asks for each key the first time the server starts and stores it securely. Leave out any you stored with login.
{
"inputs": [
{
"type": "promptString",
"id": "linear-api-key",
"description": "Linear: Personal API key",
"password": true
},
{
"type": "promptString",
"id": "github-token",
"description": "GitHub: Personal access token",
"password": true
},
{
"type": "promptString",
"id": "google-client-secret",
"description": "Google: OAuth client secret",
"password": true
},
{
"type": "promptString",
"id": "slack-bot-token",
"description": "Slack: Bot token (xoxb-\u2026)",
"password": true
},
{
"type": "promptString",
"id": "firecrawl-api-key",
"description": "Firecrawl: API key",
"password": true
},
{
"type": "promptString",
"id": "tavily-api-key",
"description": "Tavily: API key",
"password": true
},
{
"type": "promptString",
"id": "stripe-api-key",
"description": "Stripe: Secret or restricted key",
"password": true
},
{
"type": "promptString",
"id": "granola-api-key",
"description": "Granola: API key",
"password": true
},
{
"type": "promptString",
"id": "notion-api-key",
"description": "Notion: Integration secret (ntn_\u2026)",
"password": true
}
],
"servers": {
"marketing": {
"type": "stdio",
"command": "marketing-ops-mcp",
"env": {
"LINEAR_API_KEY": "${input:linear-api-key}",
"GITHUB_TOKEN": "${input:github-token}",
"GOOGLE_CLIENT_SECRET": "${input:google-client-secret}",
"SLACK_BOT_TOKEN": "${input:slack-bot-token}",
"FIRECRAWL_API_KEY": "${input:firecrawl-api-key}",
"TAVILY_API_KEY": "${input:tavily-api-key}",
"STRIPE_API_KEY": "${input:stripe-api-key}",
"GRANOLA_API_KEY": "${input:granola-api-key}",
"NOTION_API_KEY": "${input:notion-api-key}",
"GOOGLE_CLIENT_ID": ""
}
}
}
}Cursor (.cursor/mcp.json) starts it the same way:
{
"mcpServers": {
"marketing": {
"command": "marketing-ops-mcp"
}
}
}Codex (~/.codex/config.toml) starts a turn without waiting for a server unless it is required, and then the agent has none of its tools. required = true makes the session wait for it, and startup_readiness = "catalog" waits for its tool list rather than just its connection:
[mcp_servers.marketing]
command = "marketing-ops-mcp"
required = true
startup_readiness = "catalog"
startup_timeout_sec = 30Name the server marketing. A host builds each tool's name from that key, and a longer one can push a tool past the 64 characters a function name allows.
Built with Charter
Every tool here is a Charter declaration: a Pydantic schema saying where each field goes on the wire. Charter's runtime builds the request, attaches and refreshes the credential, and trims the response before the model reads it. It runs in your process, with no proxy and no telemetry.
The 27 tool schemas come to 56,044 tokens.
The same tools work in your own agent, without MCP:
from charter.adapters.openai import to_openai_tools
from charter_packs_mcp import FAMILIES
tools = FAMILIES["marketing"].tools()
definitions = to_openai_tools(tools) # or charter.adapters.langchainNeed an API that isn't here? Write a pack: your coding agent writes the declarations, and Charter's conformance suite checks them.
Linear:
linear_issues_list,linear_issue_createGitHub:
github_releases_list,github_repos_list_commitsGoogle Docs:
gdocs_documents_createSlack:
slack_chat_post_message,slack_chat_schedule_messageFirecrawl:
firecrawl_monitor_create,firecrawl_extract,firecrawl_map,firecrawl_scrape,firecrawl_crawl,firecrawl_crawl_statusGoogle Sheets:
gsheets_spreadsheets_values_append,gsheets_spreadsheets_values_updateTavily:
tavily_searchGoogle Forms:
gforms_forms_create,gforms_forms_responses_listGoogle Calendar:
gcalendar_events_insertGmail:
gmail_messages_send,gmail_drafts_createStripe:
stripe_customers_list,stripe_checkout_sessions_listGranola:
granola_notes_list,granola_notes_transcript_getNotion:
notion_data_sources_query,notion_pages_create
License
Apache 2.0.
Available Tools
29 toolsconnectA
Connect one app this server uses. For an app that issues keys, says where to get one and the terminal command that stores it. For Google, once the user's own OAuth client is set, starts the browser sign-in and returns at once: the user approves in the browser and the next call works. To see which apps are connected, call connection_status. Never ask the user for a key in the chat.
Inputs: app (required).
connection_statusARead-only
See which apps this server is connected to, and how to connect each one that is not. Changes nothing.
Inputs: no named parameters.
firecrawl_crawlC
Recursively crawl a website and scrape each discovered page.
Inputs: url (required), delay, limit, prompt, +15 more.
firecrawl_crawl_statusBRead-onlyIdempotent
Get the status and results of a crawl job.
Inputs: id (required).
firecrawl_extractA
Extract structured data from one or more URLs using an LLM. Poll results with extract_status.
Inputs: urls (required), prompt, schema, showSources, +6 more.
firecrawl_mapB
Discover and list all URLs of a website starting from a base URL. Use when the user wants to explore a site's structure or find specific pages.
Inputs: url (required), limit, search, sitemap, +7 more.
firecrawl_monitor_createB
Create a scheduled monitor for scrape, crawl, or search targets.
Inputs: name (required), targets (required), schedule (required), goal, +4 more.
firecrawl_scrapeB
Scrape a single URL and optionally extract information. Use when the user wants to read or summarize a specific webpage. Supports markdown, HTML, screenshots, and structured JSON extraction.
Inputs: url (required), proxy, maxAge, minAge, +22 more.
gcalendar_events_insertC
Create a calendar event; returns details of the event.
Inputs: event (required), calendarId (required), sendUpdates, maxAttendees, +3 more.
gdocs_documents_createA
Create a blank document with a title. Only the title is honoured — the document is created empty. To add content, call this and then documents_batch_update with the returned documentId.
Inputs: body.
gforms_forms_createA
Create a new form from a title. Only the title and the document title are honoured: the form is created with no description, no items and default settings. To add questions, call this and then forms_batch_update with the returned formId. Pass unpublished=true to create a form that does not yet accept responses.
Inputs: body (required), unpublished.
gforms_forms_responses_listARead-onlyIdempotent
List a form's submitted responses, newest page first, up to 5000 per page. The only supported filter is on submission time: pass filter='timestamp >= 2026-01-01T00:00:00Z' to read what has arrived since a point in time. Answers come back keyed by questionId.
Inputs: formId (required), filter, pageSize, pageToken.
github_releases_listARead-onlyIdempotent
List a repository's releases, newest first. Tags that were never made into releases do not appear here — repos_list_tags has those. Drafts are visible only with push access.
Inputs: repo (required), owner (required), page, perPage.
github_repos_list_commitsARead-onlyIdempotent
List commits, newest first. Narrow with sha for a branch, path for one file's history, or since/until for a window.
Inputs: repo (required), owner (required), sha, page, +5 more.
gmail_drafts_createC
Save an email draft to Gmail.
Inputs: body (required), userId.
gmail_messages_sendC
Send an email via the Gmail API.
Inputs: body (required), userId.
granola_notes_listARead-onlyIdempotent
List meeting notes, filtered by when they were created or last updated and optionally narrowed to one folder and its subfolders. Returns each note's id, title, owner and timestamps, not its content. Fetch that with notes_get. Only notes that already have a generated AI summary appear here.
Inputs: cursor, folderId, pageSize, createdAfter, +2 more.
granola_notes_transcript_getARead-onlyIdempotent
Read a meeting transcript one page at a time. Use this for any long transcript, and whenever notes_get answers 413 TRANSCRIPT_TOO_LARGE. Each item is one line of speech with who said it and when.
Inputs: noteId (required), cursor, pageSize.
gsheets_spreadsheets_values_appendC
Appends values to a spreadsheet.
Inputs: range (required), valueRange (required), spreadsheetId (required), valueInputOption (required), +4 more.
gsheets_spreadsheets_values_updateC
Sets values in a range of a spreadsheet.
Inputs: range (required), valueRange (required), spreadsheetId (required), valueInputOption (required), +3 more.
linear_issue_createA
Create an issue. team_id and title are required; everything else is optional. The UUIDs for team, assignee, state and labels come from teams_list, users_list and workflow_states_list — Linear does not accept names here. Set parent_id to create a sub-issue.
Inputs: variables (required).
linear_issues_listARead-onlyIdempotent
List issues, optionally filtered. Conditions on one filter object combine with AND. To find a team's open work, filter on team.key and state.type.
Inputs: variables.
notion_data_sources_queryB
Get the rows of a data source, optionally filtered and sorted. The filter grammar is one condition per column type, composed with and and or up to two levels deep. Read the schema first if you do not know the column names — a filter naming a column that is not there is a 400.
Inputs: dataSourceId (required), body, filterProperties.
notion_pages_createA
Create a page — as a subpage of another page, or as a row of a database by giving its data_source_id as the parent. Content comes as a markdown string Notion parses into blocks, or from a template: one or the other, never both.
Inputs: body (required), filterProperties.
slack_chat_post_messageA
Send a message to a Slack channel, private group, or DM. Provide text for a plain message; set thread_ts to reply inside an existing thread.
Inputs: channel (required), text, parse, blocks, +13 more.
slack_chat_schedule_messageA
Schedule a message for later, up to 120 days ahead. The returned scheduled_message_id is what cancels it.
Inputs: text (required), postAt (required), channel (required), threadTs.
stripe_checkout_sessions_listARead-onlyIdempotent
List checkout sessions. Stripe has no payment_status filter, so find paid ones by reading the results.
Inputs: limit, status, created, customer, +5 more.
stripe_customers_listARead-onlyIdempotent
List customers, most recently created first. Filter by email to find one.
Inputs: email, limit, endingBefore, startingAfter.
tavily_searchA
Execute a real-time web search optimized for AI agents. Use when sources are unknown or current web context is needed. Prefer search_depth advanced with chunks_per_source 3 for stronger evidence per source.
Inputs: query (required), topic, country, endDate, +21 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 29 tools
Tools are namespaced by app (linear_, github_, firecrawl_, etc.) with a distinct resource+action per tool, so cross-app confusion is minimal. The only mild overlap is within firecrawl (scrape's 'optional structured extraction' vs extract) and the meta pair connect/connection_status, but descriptions differentiate them clearly.
The dominant pattern is consistent app_resource_action with the verb last (linear_issues_list, gmail_messages_send, stripe_customers_list). Deviations are minor: inconsistent pluralization (linear_issues_list vs linear_issue_create) and the two meta tools (connect, connection_status) that abandon the pattern entirely.
At 29 tools this is on the heavy side of the rubric, though the breadth is explained by spanning 13+ distinct apps, each contributing only 1-3 tools. It feels borderline: justified by multi-app scope but still a large surface an agent must navigate.
Coverage is a thin slice per app and several descriptions point to tools that do not exist in this set: linear_issue_create references teams_list/users_list/workflow_states_list, gdocs_documents_create references documents_batch_update, gforms_forms_create references forms_batch_update, and granola_notes_list references notes_get. These dangling references create dead ends that will cause agent failures.
Maintenance
Related MCP Connectors
AI marketing team for founders, callable from any agent: plan what to say, edit video, generate images and clips, write and design posts, publish to LinkedIn, X, Instagram, TikTok, YouTube, Facebook, Threads, Bluesky, Discord and Mastodon and read what performed. OAuth sign-in, no API key; every draft waits for approval unless you say publish now. Free Forever plan: 10 posts a day, no card.
Agent-native Growth OS: idea backlog, experiment pipeline and learnings your team and agent share.
Marketing MCP: your AI agent runs your organic growth loop — you approve before it ships.
Launch your clients' products on up to 100 directories each from Claude, ChatGPT, Cursor or VS Code.
Related MCP Servers
- AlicenseBqualityBmaintenanceRuns 20 slash-command workflows across Google Calendar, Gmail, Linear, Slack, Granola, Google Docs, GitHub, Stripe, Notion, Google Forms, Sheets and Drive to produce morning briefs, meeting prep, action items assigned to owners and weekly updates. Reads proceed without asking, while anything that creates, sends, changes or deletes is shown for approval first.471Apache 2.0
- AlicenseBqualityBmaintenanceEnables freelancers and agencies to run eight back-office workflows across Stripe, Google Drive, Linear, Google Calendar, Gmail, GitHub, Google Docs, Granola, Google Sheets and Firecrawl, covering client onboarding, invoices from calendar and commits, status reports, scope-creep detection, site audits and overdue invoice chasers. Reads run freely, while anything that creates, sends, changes or deletes is shown for approval first, with credentials kept in your own OS keychain and no proxying through any third-party server.28Apache 2.0
- AlicenseBqualityBmaintenanceAn MCP server that turns CI failures, pull requests, security alerts, and Slack threads into Linear issues, Slack posts, changelogs, docs, and reports with evidence attached, through 25 agent-run workflows over 59 tools spanning GitHub, Linear, Slack, Notion, Google Docs, Google Calendar, Google Sheets, Firecrawl, and Tavily. It also handles credential setup in the OS keychain and pauses for approval before any create, send, change, or delete call.611Apache 2.0
- AlicenseBqualityBmaintenanceEnables an agent to run 20 product-management workflows that convert call notes, specs, customer feedback and competitor changes into deduped Linear issues, PRD drafts, roadmap sheets, digests and weekly project updates. Orchestrates only the tools each workflow needs across Granola, Linear, Slack, Notion, GitHub, Stripe, Firecrawl, Tavily and Google Workspace, using credentials kept in the user's own OS keychain.511Apache 2.0