drupal-mcp-connector
v5.4.5
Published
MCP server for Drupal: connects MCP clients to one or more sites over JSON:API or GraphQL, with governed writes and an SSH Drush bridge. Hosted HTTPS mode checks every call with the token issuer.
Maintainers
Readme
Drupal MCP Connector
MCP server for Drupal: connects MCP clients to one or more sites over JSON:API or GraphQL, with governed writes and an SSH Drush bridge. Hosted HTTPS mode checks every call with the token issuer.
Built by Jeremy Michael Cerda ([email protected]). Maintained by Wilkes & Liberty, LLC.
If the client only shows drupal_list_sites and drupal_governance_status, the secret env vars named in config.json are unset. Confirm they are loaded (config/secrets.map or the shipped example table) and restart the MCP server. See #199.
What It Does
Drupal MCP Connector (drupal-mcp-connector) connects any Model Context Protocol client to one or more Drupal sites. It exposes Drupal content and configuration as a set of MCP tools, resources, and prompts, so an MCP client can read, audit, and (where permitted) write content through structured, governed operations instead of the admin UI:
"Find all articles missing a meta description and list them."
"Show me every user account that hasn't logged in for 90 days."
"Create 10 draft product nodes from this structured data."
"Run an SEO and accessibility audit on the article content type."
"What content types exist on the site and which are barely used?"The connector speaks two Drupal backends interchangeably — Drupal core's JSON:API and GraphQL (via GraphQL Compose) — selectable per site. It normalizes both into one canonical entity shape, so the same tools work whether a site exposes JSON:API, GraphQL, or both. An optional SSH Drush bridge adds administrative operations the HTTP APIs can't reach.
Dual-Protocol Backends
Drupal modules can also supply their own typed MCP actions through an opt-in module-tool registry. The module owns business rules and schemas; the connector discovers and forwards approved tools under Sentinel and connector policy. Existing built-in tools remain available during migration.
Each site declares which backend(s) it exposes via the api key:
"sites": {
"main": { "baseUrl": "https://example.com", "api": "jsonapi" },
"graphql_only": { "baseUrl": "https://api.example.com", "api": "graphql" },
"either": { "baseUrl": "https://example.com", "api": ["graphql", "jsonapi"] }
}apiaccepts"jsonapi","graphql", or a priority array like["graphql","jsonapi"]. Omit it to auto-detect (the connector probes both once and caches the result).- One canonical shape. Both backends return entities as
{ id, entityType, bundle, title, status, langcode, created, changed, url, fields, relationships, _backend }, so tool output is identical regardless of protocol. Every site-addressing response also includes_target: { name, baseUrl, source }so a defaulted call cannot be mistaken for another environment. - Capability-aware. Each backend advertises what it supports (read, write, delete, server-side filter/sort, revisions). GraphQL via GraphQL Compose is read-only (no mutations) and has no server-side field filter, so filters are applied client-side over a bounded fetch and flagged
approximate/truncated. Write tools against a read-only backend return a clear capability error rather than failing silently. - Writes go through JSON:API. Use a JSON:API-enabled site as the write plane; keep GraphQL as a read plane where that suits your architecture.
defaultTextFormatsets the body text format used by thebodyconvenience parameter on node writes, e.g.{ "baseUrl": "…", "defaultTextFormat": "basic_html" }. Individual calls can override it withformat. Field APIallowed_formatswins when it can be resolved (fromfield_configor, for body,base_field_override): a single allowed format is the default, and a format outside the list is refused. Without a resolved list, a create falls back tofull_html, which many governed sites deliberately do not define — and which is the most permissive core format, so setting this per site is the safer posture. An update reuses the format already stored on the field instead of that fallback. A text format is Drupal's HTML-filtering boundary, so it is worth choosing deliberately rather than inheriting.
See docs/architecture.md for the backend abstraction and docs/graphql-local-setup.md for the GraphQL specifics.
Features
124 Tools Across 28 Modules
| Module | Tools |
|--------|-------|
| Nodes | CRUD for any content type with arbitrary field support |
| Taxonomy | Vocabulary listing + full term CRUD |
| Users | List, get, create, update, block/unblock, role management (PII-gated) |
| Media | List types, CRUD, file upload, orphaned-media detection |
| GraphQL | Execute a query, schema introspection (mutation-gated) |
| Codegen | Optional graphql_compose_codegen inspect/diff/generate (generate is --dry-run text) |
| Entities | Generic CRUD for any Drupal entity type (paragraphs, commerce, webforms, …) |
| Site | Site info, content-type discovery, configured-site listing |
| Reports | Content summary, stale content, field completeness, SEO/accessibility audits, taxonomy usage, user activity, revision hotspots (10 read-only reports) |
| Drush | Cache rebuild, cron, config sync, module management, DB updates via SSH |
| Revisions | List/get entity revisions; governed revert to a prior revision |
| Moderation | Set moderation state; list content by state; observed-state discovery (content_moderation) |
| Scheduler | Set publish-on / unpublish-on dates (Scheduler module) |
| Fields | Describe a bundle's fields (type/required/cardinality, best-effort) |
| References | Resolve a human name/title to an entity UUID for relationship fields |
| Bulk | Bulk create/update with per-item partial-failure reporting |
| Translations | List, create, and revise published entity translations (revise: true on drupal_create_translation, MCP Sentinel 2.24.0+) |
| Paragraphs | Create/update/get Paragraph components for embedding in host fields |
| Nested | Governed nested-paragraph draft (drupal_draft_nested_components) — new unpublished children pinned from the host draft |
| Structure | Menu links (list/create/update, incl. parent + enabled) + custom blocks (list/create) |
| Redirects | Create active URL redirects (301/302) + update/repoint existing redirects (Redirect module) |
| Search | Best-effort content search (title match; Search API/Solr-ready) |
| Reports (extra) | Orphaned references, unpublished content, missing-field audits |
| Reports — Links & 404 | 404-log → redirect candidates, redirect-table health (chains/loops/duplicates), body-link inventory with opt-in live checking, URL-alias coverage, menu-link integrity, embedded-entity scan |
| Reports — Config & Health | Config drift, best-practice/security config linter, module audit (dev/debug + security updates), permission audit, Drupal status report, text-format safety, cache posture |
| Reports — Content Quality | Duplicate content, workflow bottlenecks, translation coverage, scheduled content, readability (Flesch), orphan pages, PII exposure (masked), structured-meta SEO coverage |
| Audit (composite) | drupal_audit_site_health — scored content/links/config dashboard with a roll-up grade |
| Config & Governance | Governed config get/list/set via the server-tool bridge; drupal_mcp_whoami tier/capability report |
Preview writes with dryRun. The node and entity create/update/delete tools accept an optional dryRun: true flag that runs the connector's checks and returns a preview of the payload, without committing anything to Drupal. The result's checks block says what was checked. Field access and entity validation are checked only when Sentinel's non-saving draft endpoint ran; see docs/tools-reference.md.
MCP Resources
Browsable, always-fresh context the client can read without calling a tool:
drupal://sites— configured site profiles (no credentials)drupal://{site}/content-types— content types with field schemasdrupal://{site}/security-policy— the active security configuration
MCP Prompts
Prompts are exposed as slash-commands in any MCP client (Claude, Gemini, Codex, and other MCP-aware agents). Two kinds ship with the connector:
Workflow templates — multi-step guided flows:
drupal-content-audit— walk through a full site content auditdrupal-full-audit— run the composite content/links/config audit and turn the scored dashboard into a prioritized action plandrupal-create-article— guided article creation with all fieldsdrupal-seo-fix— find and fix SEO gapsdrupal-user-cleanup— identify and handle inactive accounts- Module-owned workflows (
drupal-<namespace>-<workflow>) whenserverTools.modules.workflowsis enabled — seedocs/module-workflows.md
One prompt per tool — every drupal_* tool is also exposed as a
drupal-<tool> prompt (e.g. drupal-create-node, drupal-list-nodes,
drupal-report-seo-audit), derived automatically from the tool set so it never
drifts. Each prompt takes the tool's parameters as arguments and drives a single,
governed call to that tool. These are protocol-native, so they work everywhere the
prompts capability is supported — the client renders them per its own convention
(e.g. Claude Code shows /mcp__drupal__drupal-create-node).
Filesystem slash commands (/drupal-*)
MCP prompts (above) are the protocol-native slash surface. For the literal bare
/drupal-<tool> form, the connector ships generated stubs under
.agents/commands/ — a harness-agnostic tree, not a vendor rule directory.
Clients that scan .agents/commands/ (including Grok Build when this repo is
the project) pick them up automatically.
Clients that only scan a vendor home path (Claude Code ~/.claude/commands,
Grok ~/.grok/commands, Codex $HOME/.agents/skills) install from that tree
into the operator home, so no consuming project has to commit .claude/ or
.grok/. Codex uses a skill (drupal-mcp), not deprecated custom prompts:
npm run generate:commands # refresh .agents/commands/ (from a clone)
npm run install:commands # copies into ~/.claude/commands, ~/.grok/commands, and ~/.agents/skills/drupal-mcp/
# npm run install:commands -- --clients claude
# npm run install:commands -- --clients codex
# npm run install:commands -- --home /path/to/fake-home # tests / CI
# npm run install:commands -- --modules # also write stubs for module-owned toolsModule-owned tools (docs/module-tools.md) get a prompt
whenever they are visible to the caller. Their bare /drupal-<namespace>-<alias>
stubs are written only with --modules, which discovers them from the sources
in config/config.json.
From an npm install, the same files ship at
node_modules/drupal-mcp-connector/.agents/commands/. Do not copy them into an
application repo's .claude/ or .grok/ directory.
Security Model
Defense-in-depth presets, enforced connector-side and complemented by Drupal-side
governance (MCP Sentinel). Default when security is omitted:
production-strict (read-only, sensitive types denied). Local development must
set "preset": "development" explicitly.
"security": { "preset": "auditor" }| Preset | What it does |
|--------|-------------|
| development | Everything allowed, including GraphQL — local development only |
| content-editor | Create/edit content + structural entities; no deletes; no publishing; secrets/governance/account types denied |
| config-editor | content-editor + site-building config read + governed config write (developer tier) |
| auditor | Read-only; secrets/governance/account types denied; user PII redacted when user is allowed |
| production-strict | Read-only; same sensitive denylist as auditor; broad PII field redaction (default) |
| write-plane | Create/update content set for agents; no deletes; no GraphQL; publish off by default |
Additional connector-side gates (2.1+ / 2.2+):
- Entity allowlists apply to specialized tools (
drupal_*_node, media, taxonomy), not onlydrupal_entity_*. - Publish gate:
status: trueandmoderation_state: "published"requireallowPublish. Media create defaults unpublished. Published moderated node updates without a moderation state default to draft (forward revision). - Uploads only from
MCP_UPLOAD_ROOT(or the process cwd); sensitive paths (.env*,.ssh, connectorconfig.json) are refused. - HTTPS: non-loopback binds require an inbound OAuth resource server (
auth.issuer+auth.audience+ RFC 7662 introspection). Every authenticated network call is introspected at the issuer (no positive cache; issuer down is 503).MCP_AUTH_TOKENwas removed in 4.0.0 (#231); setting it is fatal at startup.MCP_ALLOW_UNAUTHENTICATED=1is loopback-only; a non-loopback bind with that flag refuses to start. A network request with no resolved identity is 401 before dispatch.GET /healthand RFC 9728 protected-resource metadata stay reachable without a token and expose no tool data. Non-loopback TLS defaults to 120 req/min rate limiting (per client address before auth; per verified principal after).X-Forwarded-Foris honored only fromMCP_TRUSTED_PROXIES/auth.trustedProxies(default: trust none). Inbound JWT scopes (and optionalauth.grants) filter which tools, resources, prompts, and sites a principal can discover or invoke. - Restricted admission: optional
auth.allowedPrincipalslimits authenticated access to exact client/subject pairs withinauth.issuer, for example[{"clientId":"pilot-client","sub":"pilot-user"}]. Both values must match the verified token;azpand RFC 7662client_id, when both present, must agree. A valid but unlisted identity receives 403 before session creation or tool dispatch. Omitting the setting preserves normal authentication; an explicit empty array denies all authenticated access. Invalid entries, null and wildcard-only identifiers prevent startup. The policy is snapshotted at startup: update the managed configuration and restart to change membership. Admission does not grant site access or tool scopes, and it does not bypass introspection, revocation or session binding. Keep real membership identifiers in private deployment configuration. - Ops listener (mesh-only, off by default):
MCP_OPS_LISTEN=<addr:port>orops.listenstarts a second socket forGET /readyandGET /metrics. It has no authentication. It may bind only to loopback or a Tailscale address (100.64.0.0/10,fd7a:115c:a1e0::/48); port 0,0.0.0.0,::, and any other public or non-mesh address are fatal.100.64.0.0/10is also carrier-grade NAT space — confirm the address is actually Tailscale, and restrict the port with Tailscale ACLs./readyis 200 only on a 2xx introspection response with an RFC 7662-shaped body plus complete hosted scope table and allowlists (401 / 404 / 5xx are 503); probes are single-flighted and cached for a few seconds./metricsis Prometheus text (request counts by route and status, deny counts by reason, 401 / 403 / 429, rate-limit buckets, in-flight) with no principals, subjects, client ids, tokens, tenants, or IPs. The public listener does not serve/readyor/metrics(normal 401 or 404).GET /healthstays the public liveness probe. - GraphQL is off by default.
drupal_graphql/ introspect requiresecurity.allowGraphql(true only on thedevelopmentpreset). Raw GraphQL results still bypass entity allowlists and field redaction — prefer JSON:API entity tools when connector policy must hold. Mutations also needallowGraphqlMutations. - Northbound data-flow budgets (2.8+): row, byte, page, request, and chained-action counters keyed by inbound principal + authoritative target (same finite defaults as MCP Sentinel), so pagination, retries, batching, or a new chain id cannot reset them. Governed reads carry the declared-ceiling / declared-destination wire contract; tune with
security.declaredCeilingandsecurity.readBudgets.
Full detail: docs/security.md and docs/security-hardening.md.
Requirements
- Node.js 20+
- Drupal 10 or 11 (JSON:API ships in core)
- For the GraphQL backend: GraphQL Compose
- For token auth (recommended): Simple OAuth
- For the Drush bridge: SSH key access to the Drupal server
Quick Start
One-liner (recommended for operators)
npx -y drupal-mcp-connector init
# aliases: npx -y drupal-mcp-connector wizard
# npx -y drupal-mcp-wizard
# CI / dogfood: npx -y drupal-mcp-connector init --yes --json
npx -y drupal-mcp-connector doctor
# CI: npx -y drupal-mcp-connector doctor --jsonThe wizard prints Cursor (.cursor/mcp.json) and Claude Code mcpServers
snippets, optional auth.grants allowlist hints, and a whoami / contract_ready
check. It writes files only with --write (never clobbers without confirm
unless --yes). Doctor prints one primary failing gate and the exact next
fix (exit non-zero on fail; --json is secret-free).
Transport presets
| Preset | URL shape | Notes |
|--------|-----------|--------|
| Local stdio (--preset local-stdio) | https://drupal.ddev.site | Client launches this connector as a subprocess. Drupal must be reachable from this machine. |
| Tailscale VPN (--preset tailscale) | https://drupal.<tailnet>.ts.net | VPN-required. The MCP client host must be on the same tailnet. Not a public internet path. |
| Public HTTPS (--preset public-https) | https://mcp.example.com/mcp | Gated / later. Operator-run MCP_TRANSPORT=https + TLS + inbound OAuth. Not required to use the Wilkes & Liberty hosted endpoints below. |
Hosted endpoints
Wilkes & Liberty operates Streamable HTTP endpoints (DEV-817):
| Environment | MCP URL | Protected-resource discovery (RFC 9728) |
|-------------|---------|-----------------------------------------|
| Production | https://mcp.wilkesliberty.com/mcp | https://mcp.wilkesliberty.com/.well-known/oauth-protected-resource |
| Staging | https://mcp.staging.wilkesliberty.com/mcp | https://mcp.staging.wilkesliberty.com/.well-known/oauth-protected-resource |
Both advertise scopes_supported: ["mcp_read"]. /mcp is an OAuth protected resource (no token → 401). Discovery is unauthenticated. The resource-specific path /.well-known/oauth-protected-resource/mcp is also served.
OAuth path per client (mcp_read only):
- Cursor — public PKCE client
cursor-mcp. Register the MCP URL; Cursor completes authorization-code + PKCE against that client. - Grok Bot —
grok-botclient / gated anonymous registration (keystone#1541). Register the same MCP URL as a remote MCP server.
Hosted /mcp Origin allowlist (DEV-953): https://cursor.com, https://www.cursor.com, https://grok.com, https://console.x.ai. Copy into auth.allowedOrigins / MCP_ALLOWED_ORIGINS. Matching is exact.
stdio is still the default local path: the client launches the connector on your machine and inherits VPN/Tailscale/localhost reachability to Drupal. See docs/mcp-clients.md.
requireGovernance: true remains the listing default for a later marketplace
entry — this repo does not submit listings. If the client only shows
drupal_list_sites and drupal_governance_status, run doctor.
tools/list and prompts/list walk sites concurrently. The wait does not
abort the site chain. Delivery is per session: a notified client's first
list waits up to 4s and returns built-ins plus ready snapshots, then
list_changed; an unnotified client (stdio 2026 without listen, every
modern HTTP 2026 list) waits for the in-flight chain (default 20s). The
whole listing — catalog wait plus a parallel 4s governance re-check — is
hard-capped at 25s. Order on each site: governance → tools/list →
onTools; prompt hops start after governance. See
docs/module-tools.md.
Cursor plugin (stdio)
Local skeleton only (.cursor-plugin/plugin.json + root mcp.json): the client
launches npx -y drupal-mcp-connector on this machine. The plugin does not
ship a remote url.
npx -y drupal-mcp-connector init --preset local-stdio
npx -y drupal-mcp-connector doctorrequireGovernance: true is the listing default. --preset public-https stays
gated. Marketplace listing submit is operator-only — this repo does not submit.
See PRIVACY.md.
Two-minute happy path
After init and a green doctor, in the MCP client (real tool names):
- Who am I? Call
drupal_mcp_whoami. Confirmtier,preset, andcapabilities.publish(false onproduction-strict/content-editor). - What can I see? Call
drupal_list_sites, thendrupal_list_nodeswith{ "type": "article", "status": false, "limit": 5 }(unpublished only) ordrupal_get_nodeon a staging UUID. Preferdrupal_search_contentfor a title substring. - Safe unpublished write (optional, non-prod): Preview with
drupal_create_node{ "type": "article", "title": "MCP draft", "status": false, "dryRun": true }. Commit withoutdryRunonly on staging, stillstatus: false(ormoderationState: "draft"). Never setstatus: trueormoderationState: "published"unlessdrupal_mcp_whoamishowscapabilities.publish: trueand you intend to go live.
Fail-closed: default production-strict is read-only. A deny is expected when
the allowlist or Sentinel profile excludes the type — run doctor and
drupal_governance_status; do not auto-widen allowlists.
From npm (recommended for operators)
npm install -g [email protected]
# or: npx -y [email protected]Point your MCP client at the installed binary (path from which drupal-mcp-connector
or node_modules/.bin/drupal-mcp-connector). For multi-site or non-env config,
clone the repo (or copy config/config.example.json beside a small launcher) and
pass a config path — see docs/getting-started.md.
Before you upgrade
Pin the version you run. Do not point a client you depend on at latest or an
unbounded range.
- Pin an exact version in the lockfile (
package-lock.jsonornpm-shrinkwrap.json), or inpackage.jsonas"drupal-mcp-connector": "5.4.5"(use the version you intend to run). - Confirm the registry checksum before you install.
npm view [email protected] dist.integrityprints the integrity npm checks. A lockfile install fails when the tarball does not match. - Read CHANGELOG.md for that version.
- Run
npm audit --omit=devagainst the lockfile and read any advisory before you upgrade. There is no separate scanner to install.
Restart the MCP client after the upgrade. Run drupal-mcp-doctor when the
package is on your PATH, or npx -y [email protected] doctor when
you launch it with npx.
From a git clone (development)
git clone https://github.com/Wilkes-Liberty/drupal-mcp-connector
cd drupal-mcp-connector
npm install
cp config/config.example.json config/config.json
# Edit config/config.json — baseUrl, api backend, auth, security preset
node src/index.jsRegister with an MCP client
Most desktop and CLI MCP clients launch the connector over stdio:
{
"mcpServers": {
"drupal": {
"command": "drupal-mcp-connector",
"env": {
"DRUPAL_BASE_URL": "https://mysite.com",
"DRUPAL_API_TOKEN": "your-token-here"
}
}
}
}Or with an absolute path to src/index.js / the installed package. For multi-client
or remote use, run the HTTPS transport and register the endpoint instead — see
docs/getting-started.md and
docs/mcp-clients.md.
The same entry point serves current MCP 2026-07-28 clients and 2025-era clients.
Current HTTP requests are stateless and never receive Mcp-Session-Id; legacy
HTTP clients retain their existing session. Set MCP_LEGACY_TRANSPORT=reject
to end the compatibility window deliberately. The /mcp URL does not change.
Companion Drupal Module — MCP Sentinel
The connector works out of the box against Drupal core's JSON:API and a GraphQL Compose schema. For server-side governance, pair it with the MCP Sentinel module (drupal/mcp_sentinel), which enforces policy inside Drupal — independent of any connector configuration:
- Role-bound policy profiles (operation gates, entity allow/deny, field redaction)
- Tamper-evident audit log of every governed MCP operation, attributed to the acting account
- Content locks that prevent edits to content a human is actively editing
- OAuth scope enforcement (
mcp_read/mcp_write/mcp_config) per tool - HMAC-signed webhooks on MCP-driven entity changes
composer require drupal/mcp_sentinel drupal/mcp_server drupal/simple_oauth
drush en mcp_sentinel mcp_sentinel_server mcp_server_tool_bridge -y
drush mcp-sentinel:setupGovernance keys off the authenticated account's role and OAuth scopes — not request headers. The connector sends an X-MCP-Client identity header purely as a log label. See the MCP Sentinel project page for the full contract.
Raw SQL is governed, or it is off
The Drush bridge runs over SSH, which is below everything above: a drush
subprocess does not make a request Drupal can govern, and most Drush commands
never load Drupal's module system at all. drupal_drush_sql_query used to
exploit that without meaning to — it called drush sql:query, so no policy
profile, no denied entity type, no field redaction and no audit entry applied
to anything it read.
It now calls drush mcp-sentinel:sql-query (mcp_sentinel ≥ 1.14), which runs
with Drupal fully bootstrapped and enforces the same profile that governs
JSON:API. Two opt-ins, both off by default, are required:
// this connector, per site
"drushSsh": {
"rawSql": "governed", // no other value enables it
"allowedCommands": ["mcp-sentinel:sql-query"] // only if you pin this list
}…plus allow_raw_sql on the policy profile in Drupal. Expect a much narrower
tool than before: one SELECT, entity tables only, no expressions, and no
SELECT * on a table carrying a redacted column. Every attempt — permitted or
refused — lands in the audit chain with its statement text.
The rest of the bridge (sql:cli, sql:dump, php:eval and anything else you
reach over SSH) is outside Drupal governance by construction. Treat SSH as
an operator channel: keep the agent's credentials off it, and pin
allowedCommands per site.
Documentation
| Doc | Description | |-----|-------------| | Getting Started | Full setup: DDEV/Lando, Simple OAuth, multi-site, transports | | MCP Clients | Wire the connector into Claude (Code/Desktop), Grok (Build/API), and OpenAI (Codex/ChatGPT) — copy-paste config per client | | OAuth client_credentials | Production OAuth deploy: scope→role mapping, JSON:API writes, config persistence, secret handling, troubleshooting | | Architecture | Backend abstraction, canonical model, and how to extend it | | GraphQL Setup | GraphQL Compose backend + local TLS notes | | Tools Reference | Full reference for all 124 tools | | Security Guide | Presets, entity access control, field redaction | | Security Hardening | Optional transport, identity, and secrets controls | | Verification | Prove a secure install: static + live checks, negative probes, evidence artefact, managed residuals | | Threat Model | Trust boundaries, threats & mitigations, residual risks, and the security-pass results | | Deployment | Run the HTTPS transport in production: Docker, systemd, launchd, reverse proxy, pre-exposure checklist | | Integration Contract | The connector ↔ Drupal-governance contract (identity, OAuth scopes, compatibility) | | Adapter Contracts | Provider-neutral evaluator / relay / approval / evidence / SoR contracts and the Drupal conformance kit | | Versioning & Stability | Semver policy: the stable surface, deprecation process, MCP protocol + Node support | | Whitepaper | Vision, personas, and use cases |
Contributing
PRs welcome. See CONTRIBUTING.md for guidelines.
Security
Found a vulnerability? See SECURITY.md. Please do not open a public issue.
License
MIT © 2026 Jeremy Michael Cerda and Wilkes & Liberty, LLC
