Skip to main content

Command Palette

Search for a command to run...

AI SKILLS as a Thin Layer Over MCP Tools

Published
13 min read
AI SKILLS as a Thin Layer Over MCP Tools

Introduction

I have been experimenting with using AI Skills as a thin layer on top of MCP-backed tools, and I think this pattern is more useful than it first appears.

At a technical level, MCP gives the model standardized access to external tools and context. That is valuable, but raw tool access is not always enough. A model may know that a tool exists, but still needs guidance on when to use it, how to use it, what the tool is for, and what “good usage” looks like in the context of a specific prompt.

That is where I am finding Skills useful.

Rather than thinking of a Skill as replacing an MCP server, I think of it as a focused instructional layer on top of one or more MCP tools. The Skill captures intent, usage conventions, and domain-specific behavior. In practice, that makes tool use more reliable and reduces the amount of prompting I need to do each time.

💡
In Codex, you can invoke a Skill explicitly using $skill_name. MCP servers do not provide that same kind of direct user-facing invocation.

An example with Oracle ORDS

To make this more practical, I built a small STDIO MCP server that exposes an Oracle ORDS REST web service on a table called JD_SB_ENTRIES. This table stores records in a second brain app. By “second brain,” I mean the usual personal knowledge tasks: capturing notes, storing ideas, tracking follow-ups, organizing knowledge, and retrieving things later in a structured way.

The ORDS side is straightforward. I registered a template for the table with handlers for GET, POST, PUT, and DELETE that map to CRUD database operations. I secured the ORDS module that the template was created in using an OAuth 2.0 client.

💡
You could also use ORDS ORDS.ENABLE_OBJECT to Auto-REST enable the JD_SB_ENTRIES table. This generates the entire CRUD API instantly, allowing you to focus entirely on the MCP/Skill interaction layer rather than writing PL/SQL backend handlers.

I built an STDIO MCP Server in Python using the Codex desktop app. STDIO MCP servers run locally on your machine. The MCP server then exposes the REST APIs to the model through a tool interface.

The model can use the MCP tool to create, read, update, and delete rows through ORDS, without needing to know the low-level details of the HTTP call each time.

It works.

But I soon found that getting the MCP server to act on my prompts was erratic (at best). I also have Office 365 linked to my Codex desktop app setup, so the model would often choose Microsoft Planner over my tool. It would also conflict with Office 365 Calendar. It's understandable, really.

A request like "add a task for tomorrow to clean the car" would make sense for MS Outlook just as much as for my second brain.

This confusion from the LLM occurs despite clear instructions in the MCP server services definition on how to use the tool.

YAML for the MCP Server Services
oauth:
  token_url: https://example.adb.us-chicago-1.oraclecloudapps.com/ords/demo/oauth/token
  scopes: ""

services:

  • id: jd_sb_entries name: JD Second Brain Tasks, Notes, and Reminders base_url: https://example.adb.us-chicago-1.oraclecloudapps.com/ords/demo/mcp/ description: "Manage second-brain entries from natural user requests. Use this service when the user wants to add, create, save, list, review, update, or delete notes, tasks, ideas, knowledge entries, or reminder-style entries with a due date. Create new entries at jd_sb_entries and update or delete existing entries at jd_sb_entries/{entry_id}. This stores reminders as second-brain tasks or notes; it does not create real Planner or calendar reminders. Infer the correct action from conversational requests whenever possible." default_headers: Accept: application/json timeout_seconds: 30 pagination: default_page_size: 100 max_page_size: 250 max_pages_per_call: 10 max_items_per_call: 500 examples:
    • "Use rest_mcp_server to add a todo for tomorrow: clean car."
    • Add a new task reminding me to review the ORDS spec tomorrow.
    • Save a reminder for tomorrow to review the ORDS spec.
    • Create a note about MCP server pagination and save the full details.
    • "Add this to my second brain: review the ORDS spec tomorrow."
    • Show me my second-brain entries.
    • Update entry 123 to mark it high urgency.
    • Delete entry 456. columns:
    • name: entry_id data_type: NUMBER nullable: false writable: false description: Primary key identity column.
    • name: subject data_type: VARCHAR2(255) nullable: false writable: true description: Short subject line.
    • name: entry_type data_type: VARCHAR2(30) nullable: false writable: true description: Entry classification. enum_values:
      • IDEA
      • TASK
      • NOTE
      • KNOWLEDGE
    • name: ai_summary data_type: VARCHAR2(32767) nullable: false writable: true description: AI-generated summary.
    • name: user_content data_type: CLOB nullable: false writable: true description: Full entry body.
    • name: urgency data_type: VARCHAR2(30) nullable: true writable: true description: Optional urgency. enum_values:
      • LOW
      • MEDIUM
      • HIGH
    • name: action_required data_type: VARCHAR2(1) nullable: false writable: true description: Whether action is required. enum_values:
      • Y
      • N
    • name: due_date data_type: DATE nullable: true writable: true description: Optional due date in YYYY-MM-DD format.

The model still needs to understand what the API represents in business terms, how it should behave when used, and which requests should trigger a call. A generic CRUD interface is flexible, but also vague.

💡
One enhancement I thought of was to reference the OpenAPI/Swagger endpoint that ORDS makes available in the SKILL.md file. This makes the skill more resilient to changes in the API.

Using a Skill for a second brain workflow

To counter this vagueness, I decided to create a skill focused specifically on the second brain ORDS API.

Agent Skills are folders of instructions, scripts, and resources that agents can discover and use to do things more accurately and efficiently.

This turned out to be more useful than I expected.

The Skill did three important things.

1. It guided the use of the REST API

The MCP tool exposed the API's mechanics. The Skill explained how to use it.

That distinction matters.

The tool knew how to call the endpoint. The Skill told the model when to create a note, when to update an existing item rather than insert a new one, which fields mattered, and how to interpret user requests in the context of a second brain.

Without that layer, the model has to infer too much from the tool signature and endpoint description. Sometimes that works. Sometimes it does not. The more domain-specific the workflow becomes, the more that gap shows up.

In practice, the Skill reduced a lot of that ambiguity.

2. It documented second brain functionality

The Skill also became a compact form of documentation.

Instead of only documenting the REST API as a technical interface, the Skill documented the behavior around the API. It explained what the second brain supports, the kinds of operations it is intended for, and the conventions the model should follow.

That is useful for the model and for me.

It gave me a single place to describe the intended workflow in practical terms rather than just API terms. In other words, it documented capability, not just transport.

I think this is an underrated part of Skills. They are not only prompt helpers. They can also serve as executable documentation for an AI-facing workflow.

3. It allowed explicit invocation with $skill

This was the third benefit, and in some ways, the most practical.

Because the behavior was packaged as a Skill, I could explicitly invoke it with $skill_name.

That gave me a clean way to direct the model toward a very specific behavior package. I was not just hoping the model would choose the right MCP tool based on a vague request. I could point it at the exact Skill that I knew would work with that second brain API.

That explicit invocation made the interaction more predictable.

SKILL.md
---
name: "second-brain"
description: "Use when the user wants to add, update, list, or delete second-brain notes, tasks, ideas, knowledge items, or reminder-style entries through the local rest-mcp server. Prefer this skill when the user explicitly says $second-brain."
---

Second Brain

Use this skill for second-brain CRUD work through the local rest-mcp MCP server.

Core Rules

  • Use service_id: "jd_sb_entries".
  • Use path: "jd_sb_entries" for create and list. Use path: "jd_sb_entries/{entry_id}" for a specific row.
  • For filtered GET requests, use ORDS q filter syntax, not ad hoc column query params.
  • Preferred form: pass query as a native object and pass query.q as a native object. The MCP server will JSON-encode q.
  • Accepted alternate form: pass query as a raw query string such as q={"entry_type":{"$eq":"TASK"}}&limit=25.
  • Do not send second-brain filters as top-level keys like "entry_type": "TASK" unless the service explicitly documents that parameter.
  • For structured arguments, verify body and headers are native objects before calling the tool. For query, prefer a native object unless a raw query string is more direct.
  • Use page_limit and item_limit for pagination.
  • If fields, enum values, or filter keys are unclear, call rest-mcp.describe_service once. Do not retry blindly with alternate query formats.
  • Use ORDS operators inside q as needed: \(eq, \)ne, \(instr, \)like, \(gte, \)lte, \(or, \)and.
  • Keep list results compact and results-focused.
  • Never paste raw MCP response JSON into the user-facing reply. Extract the needed fields and summarize.

Fixed Playbooks

  • If the user asks to show, list, pull, or review active todos/tasks/reminders, make exactly one GET call with:
{
  "service_id": "jd_sb_entries",
  "method": "GET",
  "path": "jd_sb_entries",
  "query": {
    "q": {
      "entry_type": {
        "$eq": "TASK"
      },
      "action_required": {
        "$eq": "Y"
      }
    }
  },
  "page_limit": "1",
  "item_limit": "25"
}
  • For that active-task flow, do not probe with alternative query formats, do not call describe_service, and do not say "retrying" unless an unexpected runtime error actually occurred.
  • After fetching active tasks, sort by due_date ascending before replying unless the user asks for a different order.
  • Reply with only the compact task list: #entry_id subject — due YYYY-MM-DD.

Batch Rules

  • Default to single-item mode.
  • Enter batch mode only when the user clearly asks for multiple items or refers to a concrete earlier list.
  • For prior-thread items, restate a compact working list in the current turn before writing.
  • If the earlier items are missing or ambiguous, ask the user to narrow the scope or restate them.
  • Process at most 5 items per turn unless the user explicitly asks for more.
  • Create or update sequentially, one request_resource call per item.
  • If a batch partially succeeds, report completed items and the first failure clearly.

Field Mapping

  • todo, task, reminder -> entry_type: "TASK"
  • note -> entry_type: "NOTE"
  • idea -> entry_type: "IDEA"
  • knowledge -> entry_type: "KNOWLEDGE"
  • For todos/reminders, default action_required to "Y".
  • Default urgency to "LOW" unless the user says otherwise.
  • Use title case for subject unless the user specifies exact casing.
  • Use the raw user text or a slightly cleaned version for user_content.
  • Create a short ai_summary from the request.
  • Convert relative dates like tomorrow into an absolute YYYY-MM-DD date using the user's locale timezone.
  • Treat returned due_date values as ISO timestamps and present them back to the user as dates when only the date matters.

Request Patterns

Create:

{
  "service_id": "jd_sb_entries",
  "method": "POST",
  "path": "jd_sb_entries",
  "body": {
    "subject": "Clean car",
    "entry_type": "TASK",
    "ai_summary": "Reminder to clean the car tomorrow.",
    "user_content": "clean car",
    "urgency": "LOW",
    "action_required": "Y",
    "due_date": "2026-03-15"
  }
}

List active tasks:

{
  "service_id": "jd_sb_entries",
  "method": "GET",
  "path": "jd_sb_entries",
  "query": {
    "q": {
      "entry_type": {
        "$eq": "TASK"
      },
      "action_required": {
        "$eq": "Y"
      }
    }
  },
  "page_limit": "1",
  "item_limit": "25"
}

Search for entries containing a phrase:

{
  "service_id": "jd_sb_entries",
  "method": "GET",
  "path": "jd_sb_entries",
  "query": {
    "q": {
      "$or": [
        {
          "subject": {
            "$instr": "ORDS"
          }
        },
        {
          "user_content": {
            "$instr": "ORDS"
          }
        }
      ]
    }
  },
  "page_limit": "1",
  "item_limit": "25"
}

Read one row:

{
  "service_id": "jd_sb_entries",
  "method": "GET",
  "path": "jd_sb_entries/32"
}

Bad query examples:

"query": {
  "entry_type": "TASK",
  "action_required": "Y"
}
"query": "{\"entry_type\":{\"$eq\":\"TASK\"}}"

Good raw query-string example:

"query": "q={\"entry_type\":{\"\(eq\":\"TASK\"},\"action_required\":{\"\)eq\":\"Y\"}}&limit=25"

Response Style

  • For simple creates, reply with the created entry_id, subject, and due date.
  • For list/read requests, return the concise result only. Do not echo tool payloads, headers, links, or pagination blobs.
  • Keep the response short.
  • If the request is ambiguous, ask one concise clarifying question.

Demo

This recording shows a brief interaction with my 2nd brain after introducing the skill.

Demo showing use of 2nd brain from the Codex app

Why this pattern matters

The broader point is that MCP and Skills solve different problems.

💡
MCP is about tool access. Skills are about tool usage.

I like this analogy from the Anthropic "The Complete Guide to Building Skills for Claude".

The kitchen analogy.

MCP provides the professional kitchen: access to tools, ingredients, and equipment. Skills provide the recipes: step-by-step instructions on how to create something valuable.

If you only expose a tool, you are giving the model capability. If you add a Skill, you are giving it operating guidance. For simple tools, that extra layer may not matter much. For anything with workflow, conventions, or domain context, it matters a lot.

That is why I think Skills work well as a thin layer on top of MCP-backed tools.

  • They do not replace the server.

  • They do not replace the API.

  • They do not replace good tool design.

What they do is close the gap between “the model can call this” and “the model knows how this should be used here.”

Conclusion

A lot of MCP discussions focus on exposing tools, which makes sense. But once you start building real workflows, raw tool exposure is only the starting point. You also need a way to shape behavior around those tools.

For me, Skills are proving to be a good way to do that.

In this case, a simple STDIO MCP server exposed ORDS REST APIs for CRUD operations on a table. The Skill sitting on top of one of those APIs made the setup much more usable by guiding the workflow, documenting the behavior, and providing an explicit invocation surface via $skill.

That is a small design choice, but it has made the overall system feel much more intentional.