Poiesis is a production-ready agile project management platform designed to be controlled via the Model Context Protocol (MCP). It provides AI agents with structured tools and workflows to manage projects, epics, stories, and tasks in a fully modular architecture.
- Hierarchical Project Structure: Projects → Epics → Stories → Tasks
- Unified Status Lifecycle:
draft→open→closed(with re-open) - Dependency Tracking: Declare dependencies between artifacts; check blockers before opening work
- Module System: Extend platform capabilities with pluggable modules
- Multi-tenancy (SaaS): Row-level tenant isolation with automatic Global Scope — transparent to API consumers
- Multi-user Collaboration: Two-tier access control — global user roles and per-project membership roles
- RESTful API: HTTP-based project management endpoints
- MCP Server: Native MCP 2.0 integration for AI agent control
The MCP server exposes:
- 36+ Tools: Full CRUD operations for projects, epics, stories, tasks, modules, and dependencies
- 2 Resources: Project overview and configuration snapshots
- 1 Prompt: Agile workflow guide for AI agents
- HTTP Transport: Streamable HTTP with Server-Sent Events support
- Bearer Token Auth: Secure agent-to-server communication
- PHP 8.4+
- Laravel 12
- MariaDB 11.8+ (or any compatible database)
git clone https://github.com/arthur2jolly/poiesis.git
cd poiesis
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate --seed
php artisan serveAll MCP requests require a Bearer token in the Authorization header:
curl -X POST http://localhost:8000/mcp \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1,"params":{}}'Generate a token:
php artisan artisan:token:create --name="Agent" --user-id=1- POST /mcp: JSON-RPC request handler (initialize, tools/list, tools/call, resources/list, resources/read, prompts/list, prompts/get)
- GET /mcp: Server-Sent Events stream for push notifications
list_projects— List all accessible projectsget_project— Get project detailscreate_project— Create a new projectupdate_project— Update project title/descriptiondelete_project— Delete a project (owner only)
list_epics— List epics in a projectget_epic— Get epic detailscreate_epic— Create an epicupdate_epic— Update epicdelete_epic— Delete epic and child stories
list_stories— List stories with optional filters (type, priority, status, tags)get_story— Get story with dependenciescreate_story— Create a story in an epiccreate_stories— Bulk create stories (atomic)update_story— Update story fieldsdelete_story— Delete story and child tasksupdate_story_status— Change status (draft→open→closed)list_epic_stories— List stories of a specific epic
list_tasks— List all tasks in project with filtersget_task— Get task detailscreate_task— Create a standalone or child taskcreate_tasks— Bulk create tasks (atomic)update_task— Update taskdelete_task— Delete taskupdate_task_status— Change task status
resolve_artifact— Resolve identifier (e.g.,POIESIS-1) to full objectsearch_artifacts— Full-text search in a project
add_dependency— Declare that artifact A is blocked by artifact Bremove_dependency— Remove a dependencylist_dependencies— Inspect dependency graph for an artifact
list_available_modules— List all available moduleslist_project_modules— List active modules for a projectactivate_module— Activate a module (owner only)deactivate_module— Deactivate a module (owner only)
project://{code}/overview— Project summary (epic/story/task counts, active modules)project://{code}/config— Project configuration (allowed types, priorities, active modules)
The agile-workflow prompt contains:
- Hierarchy explanation (Project → Epic → Story → Task)
- Status lifecycle
- Workflow best practices
- Dependencies and modules guide
- Conventions and usage patterns
Clients can fetch this prompt via prompts/get to provide LLMs with structured guidance.
app/Core/
├── Mcp/
│ ├── Contracts/
│ │ ├── McpToolInterface.php # Tool provider contract
│ │ ├── McpResourceInterface.php # Resource provider contract
│ │ └── McpPromptInterface.php # Prompt provider contract
│ ├── Tools/
│ │ ├── ProjectTools.php # Project CRUD
│ │ ├── EpicTools.php # Epic management
│ │ ├── StoryTools.php # Story management
│ │ ├── TaskTools.php # Task management
│ │ ├── ArtifactTools.php # Search & resolve
│ │ ├── DependencyTools.php # Dependency graph
│ │ └── ModuleTools.php # Module activation
│ ├── Resources/
│ │ ├── ProjectOverviewResource.php
│ │ └── ProjectConfigResource.php
│ ├── Prompts/
│ │ └── AgileWorkflowPrompt.php # Workflow guide
│ ├── Http/Controllers/
│ │ └── McpController.php # HTTP handler
│ ├── Server/
│ │ ├── McpServer.php # Core MCP dispatch
│ │ └── McpTransport.php # JSON-RPC codec
│ └── Routes/
│ └── mcp.php # MCP endpoint routes
└── Providers/
└── CoreServiceProvider.php # Tool/resource registration
-
Create a tool provider (e.g.,
app/Core/Mcp/Tools/CustomTools.php):namespace App\Core\Mcp\Tools; use App\Core\Mcp\Contracts\McpToolInterface; use App\Core\Models\User; class CustomTools implements McpToolInterface { public function tools(): array { return [ [ 'name' => 'custom_action', 'description' => 'Does something custom', 'inputSchema' => [ 'type' => 'object', 'properties' => [ 'param' => ['type' => 'string'], ], 'required' => ['param'], ], ], ]; } public function execute(string $toolName, array $params, User $user): mixed { return match ($toolName) { 'custom_action' => $this->doAction($params), default => throw new \InvalidArgumentException("Unknown tool: {$toolName}"), }; } private function doAction(array $params): array { // Implementation return ['result' => 'success']; } }
-
Register in
CoreServiceProvider:private function registerMcpTools(): void { $server = $this->app->make(McpServer::class); $server->registerCoreTools(new CustomTools); }
-
Create a resource provider (e.g.,
app/Core/Mcp/Resources/CustomResource.php):namespace App\Core\Mcp\Resources; use App\Core\Mcp\Contracts\McpResourceInterface; use App\Core\Models\User; class CustomResource implements McpResourceInterface { public function uri(): string { return 'custom://{id}'; } public function name(): string { return 'Custom Resource'; } public function description(): string { return 'A custom resource'; } public function read(array $params, User $user): mixed { $id = $params['id'] ?? null; return ['id' => $id, 'data' => 'value']; } }
-
Register in
CoreServiceProvider:private function registerMcpResources(): void { $server = $this->app->make(McpServer::class); $server->registerCoreResource(new CustomResource); }
-
Create a prompt provider (e.g.,
app/Core/Mcp/Prompts/CustomPrompt.php):namespace App\Core\Mcp\Prompts; use App\Core\Mcp\Contracts\McpPromptInterface; class CustomPrompt implements McpPromptInterface { public function name(): string { return 'custom-guide'; } public function description(): string { return 'Custom workflow guide'; } public function messages(): array { return [ [ 'role' => 'user', 'content' => [ 'type' => 'text', 'text' => file_get_contents(resource_path('mcp/custom.md')), ], ], ]; } }
-
Create content in
resources/mcp/custom.md -
Register in
CoreServiceProvider:private function registerMcpPrompts(): void { $server = $this->app->make(McpServer::class); $server->registerPrompt(new CustomPrompt); }
Modules are pluggable extensions that add domain-specific tools, resources, and workflows. Each module consists of:
- Tool providers — Additional MCP tools specific to the module
- Optional resources & prompts — Domain-specific context
- Registration — Declare in the module registry with optional dependencies
Create a directory for your module (e.g., app/Modules/Reporting/):
app/Modules/Reporting/
├── MCP/
│ ├── Tools/
│ │ ├── ReportTools.php # Tools for this module
│ │ └── AnalyticsTools.php
│ ├── Resources/
│ │ └── ReportingConfigResource.php
│ └── Prompts/
│ └── ReportingGuidePrompt.php
├── Services/
│ ├── ReportService.php
│ └── AnalyticsService.php
├── Models/
│ ├── Report.php
│ └── ReportTemplate.php
└── ReportingModuleProvider.php
app/Modules/Reporting/MCP/Tools/ReportTools.php:
namespace App\Modules\Reporting\MCP\Tools;
use App\Core\Mcp\Contracts\McpToolInterface;
use App\Core\Models\User;
class ReportTools implements McpToolInterface
{
public function tools(): array
{
return [
[
'name' => 'generate_report',
'description' => 'Generate a report for a project',
'inputSchema' => [
'type' => 'object',
'properties' => [
'project_code' => ['type' => 'string'],
'format' => ['type' => 'string', 'enum' => ['json', 'pdf']],
'include_metrics' => ['type' => 'boolean'],
],
'required' => ['project_code'],
],
],
[
'name' => 'list_reports',
'description' => 'List all reports for a project',
'inputSchema' => [
'type' => 'object',
'properties' => [
'project_code' => ['type' => 'string'],
],
'required' => ['project_code'],
],
],
];
}
public function execute(string $toolName, array $params, User $user): mixed
{
return match ($toolName) {
'generate_report' => $this->generateReport($params, $user),
'list_reports' => $this->listReports($params, $user),
default => throw new \InvalidArgumentException("Unknown tool: {$toolName}"),
};
}
private function generateReport(array $params, User $user): array
{
// Implementation
return ['report_id' => 'REP-1', 'url' => 'https://...'];
}
private function listReports(array $params, User $user): array
{
// Implementation
return ['data' => []];
}
}app/Modules/Reporting/ReportingModuleProvider.php:
namespace App\Modules\Reporting;
use App\Core\Mcp\Server\McpServer;
use App\Modules\Reporting\MCP\Tools\ReportTools;
use App\Modules\Reporting\MCP\Tools\AnalyticsTools;
use Illuminate\Support\ServiceProvider;
class ReportingModuleProvider extends ServiceProvider
{
public function register(): void
{
// Bind module services
$this->app->singleton(\App\Modules\Reporting\Services\ReportService::class);
}
public function boot(): void
{
// Publish migrations, assets, etc.
$this->publishMigrations();
$this->publishConfig();
// Register MCP tools for this module
$this->registerMcpTools();
}
private function registerMcpTools(): void
{
$server = $this->app->make(McpServer::class);
// Register tools under module slug 'reporting'
$server->registerModuleTools('reporting', [
new ReportTools,
new AnalyticsTools,
]);
}
private function publishMigrations(): void
{
$this->publishesMigrations([
__DIR__.'/../database/migrations' => database_path('migrations'),
]);
}
private function publishConfig(): void
{
$this->publishes([
__DIR__.'/../config/reporting.php' => config_path('modules/reporting.php'),
]);
}
}Add to config/app.php providers:
\App\Modules\Reporting\ReportingModuleProvider::class,Create app/Core/Module/ModuleRegistry.php (or update if exists) to declare module metadata:
public function registerModule(string $slug, array $config): void
{
$this->modules[$slug] = [
'name' => $config['name'],
'description' => $config['description'],
'dependencies' => $config['dependencies'] ?? [],
];
}
// In a service provider or boot method:
$this->app->make(ModuleRegistry::class)->registerModule('reporting', [
'name' => 'Reporting',
'description' => 'Generate reports and analytics',
'dependencies' => [], // List dependent module slugs, e.g., ['core']
]);php artisan migrateOnce registered, agents can:
-
List available modules:
curl ... -d '{"jsonrpc":"2.0","method":"tools/list","params":{}}' # → Now includes tools from ReportTools and AnalyticsTools
-
Activate the module for a project:
curl ... -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"activate_module","arguments":{"project_code":"MY_PROJECT","slug":"reporting"}}}' -
Use module tools:
curl ... -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"generate_report","arguments":{"project_code":"MY_PROJECT","format":"pdf"}}}'
The agile workflow guide is stored in:
resources/mcp/agile-workflow.md
Edit this file to adjust guidelines, add module-specific instructions, or update best practices without modifying PHP code.
./vendor/bin/pint # Fix issues
./vendor/bin/pint --test # Check without fixing./vendor/bin/phpstan analyse --no-progress./vendor/bin/pestPoiesis supports row-level multi-tenancy for SaaS deployments. Each tenant has isolated users, projects, tokens, and artifacts.
- A
BelongsToTenanttrait applies a Global Scope on all tenant-aware models (User, Project, ApiToken, OAuthClient, OAuthAccessToken, OAuthAuthorizationCode, Artifact) - The tenant is resolved from the Bearer token in
AuthenticateBearermiddleware and stored in aTenantManagersingleton - All queries are automatically scoped — no code changes needed in controllers or MCP tools
- CLI commands use
withoutTenantScope()to operate across tenants
# Create a tenant (optionally create an owner user)
php artisan tenant:create "Acme Corp" --slug=acme
# List all tenants
php artisan tenant:list
# Enable / disable a tenant
php artisan tenant:enable acme
php artisan tenant:disable acme
# Delete a tenant and ALL its data (cascading)
php artisan tenant:delete acme
# Assign orphan rows (tenant_id=null) to a tenant
php artisan tenant:assign-default acme
# Create a superadmin (separate from tenant users)
php artisan superadmin:create --name=admin --password=secret123# Create a user in a specific tenant
php artisan user:create --tenant=acme --role=1
# Create a token for a user in a specific tenant
php artisan token:create john --tenant=acme --name=agentPoiesis uses two independent role systems that work together.
Assigned at user creation, controls what operations the user can perform across the entire platform (MCP tools, REST API):
--role |
Name | Create/edit artifacts | Manage projects | Manage users |
|---|---|---|---|---|
1 |
administrator |
yes | yes | yes |
2 |
manager |
yes | yes | no |
3 |
developer |
yes | no | no |
4 |
viewer (default) |
no | no | no |
php artisan user:create --role=2 # creates a managerAssigned per project, controls project-level administration:
--role |
Permissions |
|---|---|
owner |
delete project, manage members, activate/deactivate modules |
member (default) |
read/write access within the limits of the global role |
A user must have both an appropriate global role (to act on artifacts) and be a member of the project (to access it).
# Add a user to a project (project role: member, global role unchanged)
php artisan project:add-member PROJ claude.dev
# Add and set global role at the same time
php artisan project:add-member PROJ claude.manager --role=owner --policy=manager
# Update an existing member
php artisan project:update-member PROJ claude.dev --policy=developer
php artisan project:update-member PROJ claude.dev --role=owner
# List / remove
php artisan project:members PROJ
php artisan project:remove-member PROJ claude.devphp artisan migrate:fresh --seed --seeder=DevSeederphp artisan user:create [--tenant=acme] [--role=4]php artisan token:create <username> [--tenant=acme] [--name=default] [--expires=30d]- Set
APP_ENV=production - Use a strong
APP_KEY - Configure database credentials
- Enable HTTPS (MCP requires secure connections in production)
- Use managed MariaDB 11.8+ (AWS RDS, DigitalOcean, etc.)
- Run migrations:
php artisan migrate - Consider read replicas for scale
- Use Nginx with PHP-FPM
- Configure reverse proxy if behind load balancer
- Enable gzip compression
- Generate tokens via CLI:
php artisan artisan:token:create - Rotate tokens periodically
- Store securely in agent configuration
- Monitor
php artisan queue:workif using async jobs - Log MCP requests: check
storage/logs/ - Set up alerts for HTTP 5xx errors
Apache License 2.0. See LICENSE file for details.