Writing MCP Tools with Required Annotations

This document is for developers who add or maintain @rpc methods that are exposed as MCP tools. To be listed in the Claude MCP Directory, every tool must declare a safety annotation. This plugin supports that via docblock tags.

Why annotations are required

Anthropic’s MCP Directory requires every tool to have exactly one of:

  • readOnlyHint: true – the tool only reads data; it does not modify state or send external requests.
  • destructiveHint: true – the tool may modify data, write files, send requests, or have other side effects.

Without these, your server cannot be approved for the catalog. The MCPServer plugin maps docblock annotations to these MCP fields automatically.

How to annotate your RPC methods

The plugin supports exactly two safety annotations. Add one of them to the docblock of each @rpc method. Both are registered with the RPC annotation parser so they appear in parsed method info.

Docblock tag PHP constant (McpAnnotationResolver) readOnlyHint destructiveHint
@mcpReadOnly ANNOTATION_MCP_READ_ONLY ('mcpReadOnly') true false
@mcpDestructive ANNOTATION_MCP_DESTRUCTIVE ('mcpDestructive') false true

When to use @mcpReadOnly

  • Only reads from DB, files, or APIs.
  • No creates, updates, deletes, or writes.
  • No side effects (no emails, webhooks, or external calls).
  • Internal caching only is still considered read-only.

When to use @mcpDestructive

  • Creates, updates, or deletes data or resources.
  • Writes files (including temporary files).
  • Sends emails, notifications, or webhooks.
  • Calls external APIs that change state.
  • Any other side effect.

Examples

Read-only tool

/**
 * @rpc getProjectByID
 * @param int $idProject
 * @return array
 * @mcpReadOnly
 */
public function getProjectByID(int $idProject): array
{
    // Only reads from DB; no modifications.
    return $this->projectFacade->getByID($idProject);
}

Destructive tool

/**
 * @rpc createTask
 * @param string $title
 * @param int $idProject
 * @return array
 * @mcpDestructive
 */
public function createTask(string $title, int $idProject): array
{
    // Creates a new task; modifies data.
    return $this->_taskStore->create(['title' => $title, 'id_project' => $idProject]);
}

Another destructive tool (external request)

/**
 * @rpc sendNotification
 * @param string $userId
 * @param string $message
 * @return bool
 * @mcpDestructive
 */
public function sendNotification(string $userId, string $message): bool
{
    // Sends an external request (e.g. email or webhook).
    return $this->_notificationService->send($userId, $message);
}

Default when no annotation is present

If you do not add @mcpReadOnly or @mcpDestructive, the plugin defaults to destructiveHint: true (and readOnlyHint: false). That keeps existing tools valid but is conservative: catalog reviewers expect every tool to be explicitly categorized, so you should add the correct annotation for each method.

Optional: human-readable title

The MCP protocol allows an optional title for tools (a short, human-readable name for UIs). This plugin does read a title from docblocks via the @mcpTitle annotation.

  • @mcpTitle Some short label – sets the MCP tool's title field.
  • If no @mcpTitle is present, the tool has no explicit title (clients can still show name/description).

Example:

/**
 * @rpc getProjectById
 * @param int $idProject
 * @return array
 * @mcpReadOnly
 * @mcpTitle Get project by ID
 */
public function getProjectById(int $idProject): array
{
    ...
}

Summary

Docblock tag readOnlyHint destructiveHint Use for
@mcpReadOnly true false Read-only tools
@mcpDestructive false true Tools that modify or have side effects
(none) false true Default; prefer adding an explicit tag

After adding or changing annotations, run sync RPC methods so the server’s tools/list is up to date. For catalog submission, ensure every tool has the correct annotation and see ./McpClaudeServerRegistrationChecklist.md.