Skip to content
Conduit

mcp server

basehim

An open-source PHP CMS for building custom websites and APIs. Extend it with modular Apps and Themes, use its REST API, and connect AI agents through a built-in MCP server and permission-based access

basehim/basehimcommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

This project does not publish an install method we can turn into a config. Follow the README instead.

README

synced Oct 21, 2026

Basehim

The CMS built for the AI era. An open-source, API-first PHP CMS with a built-in Model Context Protocol server, so AI agents can search, read and draft your content — every permission scoped, so you decide exactly what they reach.

No Composer. No build step. No daemons. No public/ directory. Upload the files, open your site, and the installer takes it from there.

PHP MySQL License CI

basehim.com · Docs · API reference · Getting started · Download

Quick start

On a web host — download basehim.zip, upload the contents of its basehim/ folder to public_html/ (or any subfolder), and open the site. The installer asks for your database details.

On your machine — PHP 8.1+ and a MySQL or MariaDB database:

curl -L -o basehim.zip https://github.com/basehim/basehim/releases/latest/download/basehim.zip
unzip basehim.zip && cd basehim
php -S localhost:8000 server.php     # then open http://localhost:8000

Or from source: git clone https://github.com/basehim/basehim.git && cd basehim and the same php -S line.

No database handy? One container is enough:

docker run -d --name basehim-db -p 3306:3306 \
  -e MARIADB_ROOT_PASSWORD=secret -e MARIADB_DATABASE=basehim mariadb:11
# installer: host 127.0.0.1, database basehim, user root, password secret

server.php is for local development only. On a real host Apache uses the bundled .htaccess, and server.php answers 404 to anything else.


Why Basehim

Most modern CMSs assume SSH, Composer and a deploy pipeline. Basehim assumes the hosting real clients actually buy — cPanel, Plesk, plain Apache — and still gives you a first-class API surface and native agent support.

  • AI-ready by default — a built-in MCP server at /mcp. Point Claude, or any MCP client, at your site and it registers itself. No client id, no secret.
  • Scoped, revocable access — agents get OAuth 2.1 tokens carrying explicit scopes. A token issued to read posts cannot touch users.
  • API-first — a full REST API over posts, pages, media, taxonomies, menus, comments, users and settings.
  • Zero-build deployment — pure PHP. Upload a zip, run the installer.
  • Apps and themes — drop a folder in, activate, done. Marketplace built in, every download checksum-verified.
  • A readable micro-framework — autoloader, DI container with autowiring, router, view engine, hooks/events, JWT, CSRF, sessions, file cache.

AI and agents

This is the part worth reading first.

MCP server

Basehim exposes a Model Context Protocol endpoint at the root of your domain:

MethodPathPurpose
GET/mcpDiscovery — transport, auth methods, metadata URL
POST/mcpJSON-RPC 2.0 endpoint

Add https://your-site.com/mcp as a custom connector in an AI assistant and it handles registration itself. The endpoint accepts either an OAuth token or a bearer API key.

Resources are addressed with a basehim:// URI scheme — basehim://site/info, basehim://post/{id-or-slug} — and are enumerated through resources/list.

OAuth 2.1 — built for agents, not humans clicking through consoles

Agent auth sits at the domain root, outside /api/v1:

MethodPathPurpose
GET/.well-known/oauth-protected-resourceResource metadata (RFC 9728)
GET/.well-known/oauth-protected-resource/mcpPath-suffixed variant
GET/.well-known/oauth-authorization-serverServer metadata (RFC 8414)
GET/.well-known/openid-configurationSame metadata, OIDC path
POST/oauth/registerDynamic client registration
GET/POST/oauth/authorizeConsent screen and approval
POST/oauth/tokenExchange a code, or refresh

Grants: authorization_code and refresh_token, with PKCE (S256).

Dynamic client registration is the point — there is nothing to configure by hand before an agent can connect.

Scopes

Every token carries scopes, and a token issued for one thing cannot do another:

posts:read        posts:write
taxonomies:read   taxonomies:write
media:read        comments:read      comments:write
settings:read     users:read

Installed apps can contribute their own scopes, so your site's list may be longer. Whatever your site advertises is authoritative — fetch /.well-known/oauth-authorization-server to see it.

A token also acts as the account behind it: scopes choose the tools, and the account's role decides what those tools may do, exactly as on the REST API. An author's posts:write token edits that author's posts and no one else's, a contributor's goes in for review rather than live, and suspending an account stops its tokens and keys at once. Scopes added by apps are granted only to administrators unless the app maps them to a capability with the mcp.scope_capabilities filter.

Desktop agent API

For companion apps running on a machine rather than in a chat window:

Token hygiene

A token is a password. Issue the narrowest scope that does the job, use a separate token per integration so one can be revoked without disturbing the others, and keep tokens out of client-side code and screenshots. Revoking in the admin takes effect immediately.

REST API

Base URL https://your-site.com/api/v1.

  • Success responses are wrapped: {"data": …}
  • Errors follow RFC 7807 problem details: {"type", "title", "status", "detail"}
  • Single items are fetched by slug; updates and deletes address them by id. GET /posts/hello-world but DELETE /posts/12. This catches people out.
  • List endpoints accept page, per_page, status, q and author_id.

Reading published content needs no authentication. Anything that writes, or reads drafts, does.

# Public read — no auth
curl https://your-site.com/api/v1/posts

# API key
curl -H "Authorization: Bearer basehim_…" https://your-site.com/api/v1/me

# JWT
curl -X POST https://your-site.com/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"…"}'

# Create a post
curl -X POST https://your-site.com/api/v1/posts \
  -H "Authorization: Bearer basehim_…" \
  -H 'Content-Type: application/json' \
  -d '{"title":"Hello","content":"<p>World</p>","status":"published"}'

Three ways in, for three different callers:

MethodHeader / flowBest for
API keyAuthorization: Bearer basehim_…Scripts, server-to-server
JWTPOST /auth/login, refresh at /auth/refreshApps acting for a signed-in person
OAuth 2.1Dynamic client registrationAI agents over MCP

The full endpoint list — content, media, taxonomies, comments, menus, users, settings, apps, cache and scheduling — is at basehim.com/docs/api-reference, and mirrored in the admin at /admin/api/reference.

Requirements

  • PHP 8.1+ with pdo_mysql, mbstring, openssl, fileinfo, gd, zip
  • MySQL 5.7+ or MariaDB 10.3+
  • Apache or LiteSpeed with mod_rewrite (works on default cPanel). nginx works too, but you must replicate the .htaccess rules yourself — see Security.
  • For local development, PHP's built-in server with server.php is enough
  • ~30 MB disk, plus space for uploads

Install

  1. Upload the contents of the basehim/ folder from basehim.zip (or of this repository) to your document root, e.g. public_html/.
  2. Create a MySQL database and grant a user full privileges.
  3. Visit your domain — you'll be redirected to /install.php.
  4. Work through the wizard: database, site details, admin account, optional starter content.
  5. That's it. The installer locks itself when it finishes (INSTALLED=true in .env); deleting install.php afterwards is optional.

Log in at /admin/login.

rsync -avz basehim/ user@server:/path/to/webroot/

cp .env.example .env
# Edit DB_*, APP_URL, then generate the two secrets:
php -r "echo 'APP_KEY=' . bin2hex(random_bytes(16)) . PHP_EOL;"
php -r "echo 'JWT_SECRET=' . bin2hex(random_bytes(32)) . PHP_EOL;"

# Migrations apply in filename order
for f in database/migrations/*.sql; do mysql -u user -p mydb < "$f"; done

chmod -R 775 storage/ content/

Generate a password hash with:

php -r "echo password_hash('YourPassword', PASSWORD_BCRYPT, ['cost' => 12]) . PHP_EOL;"

Subdirectory installs work out of the box — Basehim detects its own base path.

Every install gets its own random table prefix (three characters and an underscore, such as k7q_), chosen by the installer and written to .env as DB_PREFIX. Several sites can share one database, which matters on shared hosting where you get a fixed number, and table names can't be guessed. To use a prefix of your own, put DB_PREFIX in .env before installing. Never change it afterwards.

Directory map

basehim/
├── index.php              # Front controller (all requests funnel here)
├── install.php            # Web installer (locks itself when done)
├── server.php             # Router for `php -S` — local development only
├── bootstrap.php          # Autoloader, env, sessions, boots the app
├── .htaccess              # Apache rewrites + security headers
├── .env                   # Your environment — git-ignored, NEVER commit
├── .env.example           # Template to copy to .env
│
├── app/
│   ├── Core/              # Router, Container, View, Cache, JWT, App base class
│   ├── Http/
│   │   ├── Controllers/   # Admin/, Api/, Web/
│   │   └── Middleware/    # Authenticate, CheckCapability, Cors
│   ├── Repositories/      # DB access layer (PDO)
│   └── Services/          # Business logic (PostService, AppService, …)
│
├── admin/views/           # Admin UI templates (Tailwind)
├── api/v1/                # REST API entry
├── config/                # app, database, auth, capabilities, cms
├── content/
│   ├── apps/              # Drop-in apps (manifest in app.json)
│   ├── themes/            # Drop-in themes (manifest in theme.json)
│   └── uploads/           # Media library
├── database/migrations/   # SQL migrations, applied in filename order
├── docs/                  # App, permission, and agent API guides
├── routes/                # api.php, admin.php, web.php
└── storage/               # Runtime: cache/, logs/, sessions/ (must be writable)

Everything under storage/ and content/uploads/ is runtime state — the directories are tracked, their contents are git-ignored.

Apps

An app is a folder in content/apps/ with an app.json manifest. Install by uploading a zip from /admin/apps, from the built-in marketplace, or by dropping the folder in — it is auto-detected.

content/apps/my-app/
├── app.json                 # Manifest (required)
├── src/
│   └── App.php              # Entry class extending App\Core\App
├── views/                   # (optional) app-owned templates
└── assets/                  # (optional) bundled css/js/images

app.json

{
    "name": "My App",
    "vendor": "myco",
    "slug": "my-app",
    "version": "1.0.0",
    "description": "What my app does.",
    "author": "Me",
    "namespace": "MyCo\\MyApp",
    "src": "src",
    "entry": "App"
}

Writing an app

Extend App\Core\App and implement boot(). Every helper below is provided by the base class — no need to dig into the DI container.

<?php
namespace MyCo\MyApp;

class App extends \App\Core\App
{
    /* -- Lifecycle (all optional) -- */
    public function onInstall(): void   { /* first detection */ }
    public function onActivate(): void  { /* admin enabled it */ }
    public function onDeactivate(): void{ /* admin disabled it */ }
    public function onUninstall(): void { /* about to be removed */ }

    /* -- Main entry: runs on every request while active -- */
    public function boot(): void
    {
        // Hooks
        $this->addAction('post.created', [$this, 'onPostCreated']);
        $this->addFilter('post.content', fn($content) => $content . '<p>👋</p>');

        // Custom routes
        $this->get('/hello', fn() => 'Hello from an app!');
        $this->post('/admin/myapp', [$this, 'saveSettings']);

        // Dashboard widget (docs/DASHBOARD-WIDGETS.md)
        $this->registerWidget('stats', [
            'surfaces'    => ['dashboard'],
            'title'      => 'Site Stats',
            'capability' => 'edit_posts',
            'dashboard'  => [
                'view' => 'stat',
                'data' => fn($ctx) => ['items' => [['label' => 'Hello', 'value' => 42]]],
            ],
        ]);

        // Admin sidebar item
        $this->addAdminMenu([
            'url' => '/admin/myapp', 'label' => 'My App', 'icon' => 'fa-star',
        ]);
    }
}

Built-in helpers

HelperWhat it does
addAction($tag, $cb, $priority?, $args?)Register an event listener
addFilter($tag, $cb, $priority?, $args?)Register a value transformer
doAction($tag, ...$args)Fire your own event
applyFilters($tag, $value, ...)Run a value through your filter chain
get($pattern, $handler) / post(...) / route([...], ...)Register HTTP routes
routeGroup($attrs, $cb)Group routes with shared prefix/middleware
addAdminMenu(['url'=>'…','label'=>'…','icon'=>'…'])Add a sidebar item
registerWidget($key, $def)Contribute a widget (editor/frontend/dashboard); dashboard cards: docs/DASHBOARD-WIDGETS.md
getSetting($key, $default = null)Read an app-scoped setting
setSetting($key, $value)Write an app-scoped setting
deleteSetting($key) / allSettings() / dropAllSettings()Manage settings
table($name)Prefixed, app-namespaced table name (app_{slug}_{name})
db()The PDO-wrapped Database instance
cache()File-based key/value cache
log($msg, $context?, $level?)Log line tagged with your app slug
view($template, $data = [])Render a PHP template from views/
asset($relativePath)URL to a bundled asset under assets/
schema($sql)Run a CREATE / ALTER (idempotent table setup)
make($class)Resolve anything from the container
config($key, $default = null)Read a config value
request() / session()Current request / session
slug() / path() / manifest()App metadata

Settings written via setSetting() are stored in the settings table under the group app:{slug}, so they can never collide with core or other apps. On uninstall, all app-owned settings are removed for you automatically.

Bundled assets are served by Basehim itself at /content/apps/{slug}/assets/{path}, with ETags and correct MIME types — no route needed. Use $this->asset('css/style.css') to build the URL.

Permissions

Apps declare what they need in app.json, and the admin sees and approves that list before activation. See docs/APP-PERMISSIONS.md.

Each app with an admin area also gets an access_app:{slug} capability, so access can be granted or denied per role and per user.

Installing an app

From the admin: /admin/apps → Upload app → pick a .zip. The installer validates the archive is readable and under 16 MB, finds app.json at the archive root or one level deep, rejects archives containing .. or absolute paths (zip-slip protection), validates the manifest, refuses to overwrite an existing app with the same slug, extracts to a temp directory, then atomically renames into place.

The app appears as inactive. Click Activate to fire onActivate(), where you'd typically create your tables.

From the marketplace: /admin/apps/marketplace — browse and install directly, every download SHA-256 verified.

Lifecycle

  • Deactivate — stops booting the app; data is preserved.
  • Delete — fires onUninstall(), removes app-scoped settings, drops the DB row, and removes files from disk.

Worked examples

  • content/apps/wp-migrator/ — the bundled WordPress Migrator, and the most complete example in the tree: a batched multi-step wizard, its own tables created on activate and dropped on uninstall, admin views with a sidebar entry, app-scoped settings, and bundled assets.
  • docs/examples/system-monitor/ — a small working app: an admin page behind the app's access capability, rendered in the admin layout, and a dashboard card with a data provider, a setting and an action.
  • docs/examples/dashboard-widgets/ — every kind of dashboard card: stat, table with settings, status with an action, a card fed by the app's own AJAX action, and an HTML widget.

Full guides live in docs/: theme development, app development, the app API, permissions.

Themes

A theme is a folder under content/themes/ with a theme.json manifest and a templates/ directory. No compile step, no framework to learn — ship one by zipping a folder.

content/themes/my-theme/
├── theme.json
├── templates/
│   ├── index.php           # Post feed
│   ├── single.php          # Single post
│   ├── page.php            # Static page
│   └── partials/
└── assets/

Themes can declare widget areas through a widget_areas map in theme.json, which then appear on /admin/widgets/areas for placement.

Two ship with core: default and dark-night.

Scheduling

Scheduled tasks are registered by apps and run through a cron entry point:

*/5 * * * * curl -s "https://your-site.com/api/v1/schedule/run?token=…"

/schedule/run sits outside the authenticated group deliberately — a crontab sends no cookies and holds no token, so it is guarded by an unguessable token instead. List tasks with GET /api/v1/schedule, or run one on demand with POST /api/v1/schedule/{app}/{key}/run.

Security

  • All requests funnel through index.php. .htaccess blocks direct access to app/, config/, storage/, database/, .env and bootstrap.php. If you run nginx you must replicate those denials yourself.
  • Passwords hashed with bcrypt, cost 12.
  • CSRF tokens on every form (_csrf field, X-CSRF-Token header).
  • JWTs (HS256) are short-lived; refresh tokens are stored as SHA-256 hashes and can be revoked.
  • API keys are stored as SHA-256 hashes — the plaintext key is shown once, at creation, and never again.
  • Sessions are file-based with HttpOnly, SameSite=Lax, and Secure when HTTPS is detected.
  • SQL via PDO prepared statements throughout, with emulation disabled.
  • Upload validation: MIME type, extension and size limit, configurable in config/cms.php.
  • App zips are checked for zip-slip before extraction.

Reporting a vulnerability and the operator hardening checklist: SECURITY.md.

Contributing

Bug reports and pull requests are welcome. See CONTRIBUTING.md for local setup, code style, and the compatibility constraints that come with targeting shared hosting: no Composer requirement, no build step, no daemons, PHP 8.1 floor.

License

Released under the MIT License.

↑↓ navigate openesc close