ai

Building an MCP server into Vortex: what it means to make your app tool-accessible

What it meant to add MCP tools to Vortex, the Laravel MCP package, how tools are structured, what good tool design actually requires, and what changed when an AI could operate the release workflow directly.

Vortex is a project management tool I built to manage the Git Flow workflow at PickYourTrail. It tracks projects, branches, releases, and reviews, the full Git Flow lifecycle from feature branch creation through production release. Earlier this month I added an MCP server to it.

MCP (Model Context Protocol) is Anthropic’s open standard for giving AI models structured access to tools, a way to expose application capabilities as callable functions with typed schemas. Adding it to Vortex meant that an AI agent connected to Vortex could actually create a hotfix branch, list in-progress releases, or complete a release, rather than just generating text about how to do those things.

This is an account of how that worked, what I learned about tool design in the process, and what’s different about your application once it has a tool surface.

What Vortex does

Before the MCP layer makes sense, the domain needs to be clear.

Vortex manages release workflow for multiple GitHub repositories. Each project has a main branch (production), a dev branch, feature branches, and hotfix branches. The release process follows Git Flow: features get merged to dev via PRs, a release branch gets created from dev, QA happens, and then the release is completed (merged to main and tagged).

The manual version of this involves GitHub UI, terminal, and keeping a mental model of where each project is in its release cycle. Vortex automates the bookkeeping: tracking branch status, recording what features are in a release, enforcing who can approve what, and integrating with GitHub’s API.

This is a domain with clear operations, real state, and defined boundaries. That made it a good candidate for MCP.

The laravel/mcp package

Laravel has a first-party MCP package, laravel/mcp, that turns a Laravel app into an MCP server. The model: you create tool classes that extend Tool, define a schema() method with the input parameters, and a handle() method with the implementation. The framework handles the MCP protocol layer.

The Tool base class:

use Laravel\Mcp\Server\Tool;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Illuminate\JsonSchema\JsonSchema;

class CreateHotfixBranchTool extends Tool
{
    protected string $name = 'create-hotfix-branch';
    protected string $title = 'Create Hotfix Branch';
    protected string $description = <<<'MARKDOWN'
        Create a new hotfix branch in a Git Flow workflow. This creates a branch
        on GitHub from the main branch (production) for critical bug fixes that
        need immediate deployment.

        **Requirements:**
        - User must have owner or maintainer role
        - Hotfix name must be unique within the project

        **What happens:**
        1. Creates branch on GitHub from the main branch
        2. Creates branch record in Vortex with 'active' status
        3. Returns branch details and GitHub branch URL

        **Next steps after creation:**
        - Fix the critical bug on this branch
        - Finish the hotfix using the `finish-hotfix` tool
    MARKDOWN;

    public function schema(JsonSchema $schema): array
    {
        return [
            'project_id' => $schema->string()
                ->description('The ID or Git URL of the project'),
            'hotfix_name' => $schema->string()
                ->description('Name of the hotfix (will be prefixed with "hotfix/"). Max 50 chars.'),
            'description' => $schema->string()
                ->description('Optional description of the hotfix')
                ->nullable(),
        ];
    }

    public function handle(Request $request): Response
    {
        $user = $request->user();
        if (!$user instanceof User) {
            return Response::error('Unauthenticated. Provide a valid Sanctum API token.');
        }

        $projectId = $request->get('project_id');
        $hotfixName = $request->get('hotfix_name');
        $description = $request->get('description');

        $project = $this->findProject($projectId);
        if (!$project) {
            return Response::error("Project not found: {$projectId}");
        }

        try {
            $branchService = app(BranchCreationService::class);
            $branch = $branchService->createHotfixBranch($project, $hotfixName, $user, $description);
            return Response::text($branchService->formatBranchCreationResponse($branch, $project, $user));
        } catch (\Exception $e) {
            return Response::error("Failed to create hotfix branch: {$e->getMessage()}");
        }
    }
}

The authentication goes through Laravel Sanctum, the same token auth the rest of the Vortex API uses. $request->user() returns the authenticated user, and the tool checks roles and permissions through Vortex’s existing authorization layer. The MCP interface is secured with the same discipline as any other production surface.

The full tool surface

After building out the release workflow, Vortex has over 40 MCP tools:

For branch operations: create-feature-branch, create-hotfix-branch, create-devops-branch, finish-feature, finish-hotfix, delete-branch, rename-branch, list-branches, get-branch-details, check-branch-conflicts, merge-to-devops, sync-feature-branches-to-devops, reset-devops-branch.

For release management: create-release, complete-release, discard-release, list-releases, get-release-details, get-release-features.

For project management: create-project, delete-project, get-current-project, get-project-details, get-project-health, list-projects, update-project-settings, add-project-member, remove-project-member, list-project-members, update-member-role, list-project-activity.

For review workflow: request-review, approve-review, reject-review, list-reviews.

For user/notification: current-user, list-active-users, list-notifications, get-notification-settings, update-notification-settings.

That’s the full operational surface of the product exposed as tools. Every significant thing you can do in Vortex’s UI, you can now do via MCP.

What tool descriptions actually do

The description field on each tool is not documentation for humans, it’s the model’s understanding of when and how to use the tool. That reframe changed how I wrote them.

Look at the CompleteReleaseTool description:

protected string $description = <<<'MARKDOWN'
    Complete an in-progress release by marking it as finished and updating project version.

    **IMPORTANT - Context Required:**
    This tool requires a specific release_id. DO NOT guess or randomly select a release ID.

    **Before calling this tool:**
    1. Call `list-releases` with status="in_progress" to get active releases
    2. Release ID will be shown in the list output
    3. If multiple releases exist, confirm with user which one to complete
    4. Verify all release requirements are met before completing
MARKDOWN;

“DO NOT guess or randomly select a release ID”, that instruction is there because in early testing, the model would sometimes call complete-release with a plausible-looking release ID rather than first calling list-releases to find the actual one. The description is behavioral instruction, not just documentation.

Similarly, GetCurrentProjectTool explains when to use it versus GetProjectDetailsTool:

Use this when:
- You're working in a local repository and want to know "What Vortex project is this?"
- You have a GitHub URL and want to find the corresponding Vortex project
- You want basic project info and quick stats

For detailed project analysis with branches/releases, use `get-project-details` instead.

This kind of disambiguation matters when you have 40+ tools. The model needs to be able to route to the right tool, and the description is how it learns which tool is appropriate for which situation. Vague descriptions → wrong tool selection → errors or hallucinated parameters.

Read-only annotations

The package supports #[IsReadOnly] and #[IsIdempotent] PHP attributes for tools that don’t modify state:

#[IsIdempotent]
#[IsReadOnly]
class ListReleasesTool extends Tool
{
    // ...
}

#[IsReadOnly] tells the MCP client that this tool has no side effects, it’s safe to call freely without confirmation. #[IsIdempotent] means calling it multiple times produces the same result. These annotations aren’t just metadata, MCP clients use them to decide whether to prompt for confirmation before calling a tool.

For Vortex, tools like list-releases, get-project-details, list-branches, and get-branch-details are read-only. Tools like complete-release, create-hotfix-branch, and delete-project are not annotated, which signals to the client that they should be called with care.

How the output is formatted

Tools return Response::text(...) with markdown-formatted strings. The ListReleasesTool builds a readable report:

$lines = [
    "# Releases for {$project->getFullRepositoryName()}",
    "",
    "**Repository**: {$project->github_url}",
    "**Total Releases**: {$totalCount}",
];

foreach ($releases as $release) {
    $lines[] = "### {$release['version']} ({$release['release_type']})";
    $lines[] = "- **Status**: " . ucfirst($release['status']);
    $lines[] = "- **Tag**: {$release['tag_name']}";
    $lines[] = "- **Features**: {$release['feature_count']}";
}

return Response::text(implode("\n", $lines));

The output is readable to both the model (which needs to parse it to decide next steps) and a human (who might be reading a transcript). Structured markdown hits that middle ground better than raw JSON.

What changes when your app is tool-accessible

Before the MCP layer, using Vortex meant opening the web UI. Creating a hotfix branch required navigating to the project, finding the branch creation form, filling it out. The application was a UI.

With MCP, it’s also a tool surface. Claude Desktop with Vortex connected can answer “what’s the current release status across all my projects?” by calling list-releases on each project in parallel. It can create a hotfix branch while I describe the bug in natural language. It can generate a release summary by calling get-release-features and compositing the output.

The interesting shift is that the application becomes legible to the AI in a way that screenshot-based or text-based interaction never achieves. The AI doesn’t have to infer that “clicking the green button creates a branch”, it knows create-feature-branch creates a branch, what parameters it takes, what errors it can return, and what to call next.

That’s the value MCP adds that plain API access doesn’t fully capture. The tool schema is the contract, and the description is the semantics. Together they let the model reason about what the application can do rather than just send HTTP requests.

Building Vortex’s MCP layer also clarified which parts of the product are genuinely useful as operations versus which parts exist only as visual interfaces. Some things that work fine in the UI became awkward as tools, they have too much context embedded in visual layout to translate cleanly. That’s useful signal about where the real application model lives versus where the UI is papering over something.