# 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.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">In Codex, you can invoke a Skill explicitly using <code>$skill_name</code>. MCP servers do not provide that same kind of direct user-facing invocation.</div>
</div>

## **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.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You could also use ORDS <code>ORDS.ENABLE_OBJECT</code> to <a target="_blank" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="https://www.thatjeffsmith.com/archive/2017/03/auto-rest-with-ords-an-overview-and-whats-next/" style="pointer-events: none;">Auto-REST</a> enable the <code>JD_SB_ENTRIES</code> 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.</div>
</div>

I built an STDIO MCP Server in Python using the [Codex desktop app](https://chatgpt.com/codex). 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.

<details data-node-type="hn-details-summary">
<summary>YAML for the MCP Server Services</summary>
<pre class="not-prose"><code class="language-yaml">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.</code></pre>
</details>

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.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">One enhancement I thought of was to reference the OpenAPI/Swagger endpoint that ORDS makes available in the <a target="_self" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="http://skill.md" style="pointer-events: none;">SKILL.md</a> file. This makes the skill more resilient to changes in the API.</div>
</div>

## **Using a Skill for a second brain workflow**

To counter this vagueness, I decided to create a [skill](https://agentskills.io/home) 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.

<details data-node-type="hn-details-summary">
<summary>SKILL.md</summary>
<pre class="not-prose"><code class="language-markdown">---
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"}}&amp;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:

```json
{
  "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` -&gt; `entry_type: "TASK"`
- `note` -&gt; `entry_type: "NOTE"`
- `idea` -&gt; `entry_type: "IDEA"`
- `knowledge` -&gt; `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:

```json
{
  "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:

```json
{
  "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:

```json
{
  "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:

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

Bad `query` examples:

```json
"query": {
  "entry_type": "TASK",
  "action_required": "Y"
}
```

```json
"query": "{\"entry_type\":{\"$eq\":\"TASK\"}}"
```

Good raw query-string example:

```json
"query": "q={\"entry_type\":{\"$eq\":\"TASK\"},\"action_required\":{\"$eq\":\"Y\"}}&amp;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.
</code></pre>
</details>

# 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](https://cdn.hashnode.com/uploads/covers/626b62127d5d27b992e4cf90/9ee8b60f-d79b-40cb-9536-ba1035f8a6c8.gif align="center")

# **Why this pattern matters**

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

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">MCP is about tool access. Skills are about tool usage.</div>
</div>

I like this analogy from the Anthropic "[The Complete Guide to Building Skills for Claude](https://resources.anthropic.com/hubfs/The-Complete-Guide-to-Building-Skill-for-Claude.pdf)".

> **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.
