Agent skill

Add Service

by cyanheads in cyanheads/pubmed-mcp-server

Scaffold a new service integration. An agent skill from cyanheads/pubmed-mcp-server.

Apache-2.0Auto-check passedResearch & Science

Install Add Service

skills CLI
$ npx skills add cyanheads/pubmed-mcp-server --skill add-service -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install cyanheads/pubmed-mcp-server add-service --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/cyanheads/pubmed-mcp-server.git skills-src && mkdir -p .claude/skills && cp -r skills-src/framework-skills/add-service .claude/skills/add-service && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
add-service
GitHub stars
158
Token cost
~3.6k tokens
SKILL.md length
1,258 words
Files
1
Skills in repo
30
Repo updated
First seen
Licence
Apache-2.0

At a glance

Scaffold a new service integration. An agent skill from cyanheads/pubmed-mcp-server.

  • Works in 6 steps: Gather the service domain name and what… → Create the directory at… → Create the service file at… → …
  • The user asks to add a service
  • SKILL.md covers Context, Steps, Template and Resilience (External API…, plus 3 more sections
  • Calls bun

What it does

Add Service is an agent skill from cyanheads/pubmed-mcp-server. Scaffold a new service integration. Use when the user asks to add a service, integrate an external API, or create a reusable domain module with its own initialization and state.

Its SKILL.md is about 3.6k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Research & Science. The repository describes itself as: Search PubMed/Europe PMC, fetch articles and full text (PMC/EPMC/Unpaywall), citations, MeSH terms via MCP. STDIO or Streamable HTTP. The licence is Apache-2.0.

When your agent uses it

  • The user asks to add a service
  • Integrate an external API
  • Create a reusable domain module with its own initialization and state

Example prompts

  • “/add-service”

Workflow steps

6 steps, taken from the first numbered list in SKILL.md.

  1. Gather the service domain name and what it integrates with from the user's request — ask only if genuinely absent
  2. Create the directory at src/services/{{domain}}/
  3. Create the service file at src/services/{{domain}}/{{domain}}-service.ts
  4. Create types at src/services/{{domain}}/types.ts if needed
  5. Register in setup() in the server's entry point (src/index.ts, or src/worker.ts for Worker-only servers)
  6. Run bun run devcheck to verify

What it can do on your machine

Read from SKILL.md and the folder at commit 79145a6. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • bun

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Add Service loads about 3.6k tokens when it runs. Until then it costs about 47 tokens; SKILL.md has 1,258 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~47
When it runs · the whole SKILL.md, loaded when a task matches
~3.6k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from cyanheads/pubmed-mcp-server at commit 79145a6, republished under its Apache-2.0 licence (© cyanheads). 1,258 words, ~3,634 tokens.

Download SKILL.mdSave it as .claude/skills/add-service/SKILL.md (or your agent's skills folder).
name
add-service
description
Scaffold a new service integration. Use when the user asks to add a service, integrate an external API, or create a reusable domain module with its own initialization and state.
metadata.author
cyanheads
metadata.version
1.12
metadata.audience
external
metadata.type
reference

Context

Services use the init/accessor pattern: initialized once in createApp's setup() callback, then accessed at request time via a lazy getter. Each service lives in src/services/[domain]/ with an init function and accessor.

Service methods receive Context for correlated logging (ctx.log) and tenant-scoped storage (ctx.state). Convention: ctx.requestInput should only be called from tool, resource, and prompt handlers, not from services — a service can call it, but the never return means TypeScript cannot narrow across it at the handler's call site.

For the full service pattern, CoreServices, and Context interface, read the framework's CLAUDE.md/AGENTS.md (loaded at session start).

Steps

  1. Gather the service domain name and what it integrates with from the user's request — ask only if genuinely absent
  2. Create the directory at src/services/{{domain}}/
  3. Create the service file at src/services/{{domain}}/{{domain}}-service.ts
  4. Create types at src/services/{{domain}}/types.ts if needed
  5. Register in setup() in the server's entry point (src/index.ts, or src/worker.ts for Worker-only servers)
  6. Run bun run devcheck to verify

Template

Service file
typescript
/**
 * @fileoverview {{SERVICE_DESCRIPTION}}
 * @module services/{{domain}}/{{domain}}-service
 */

import type { AppConfig } from '@cyanheads/mcp-ts-core/config';
import type { StorageService } from '@cyanheads/mcp-ts-core/storage';
import type { Context } from '@cyanheads/mcp-ts-core';

export class {{ServiceName}} {
  constructor(
    private readonly config: AppConfig,
    private readonly storage: StorageService,
  ) {}

  async doWork(input: string, ctx: Context): Promise<string> {
    ctx.log.debug('Processing', { input });
    // Domain logic here
    return `result: ${input}`;
  }
}

// --- Init/accessor pattern ---

let _service: {{ServiceName}} | undefined;

export function init{{ServiceName}}(config: AppConfig, storage: StorageService): void {
  _service = new {{ServiceName}}(config, storage);
}

export function get{{ServiceName}}(): {{ServiceName}} {
  if (!_service) {
    throw new Error('{{ServiceName}} not initialized — call init{{ServiceName}}() in setup()');
  }
  return _service;
}
Entry point registration

Add the setup() callback and import to the existing createApp() call — preserve the existing tool/resource/prompt arrays:

typescript
// In src/index.ts (or src/worker.ts for Worker-only servers)
import { init{{ServiceName}} } from './services/{{domain}}/{{domain}}-service.js';

// Add setup() alongside existing options:
setup(core) {
  init{{ServiceName}}(core.config, core.storage);
},
Usage in tool handlers
typescript
import { get{{ServiceName}} } from '@/services/{{domain}}/{{domain}}-service.js';

handler: async (input, ctx) => {
  return get{{ServiceName}}().doWork(input.query, ctx);
},

Resilience (External API Services)

When a service wraps an external API, apply these patterns. For the framework retry contract, see framework-skills/api-utils/SKILL.md.

Retry wraps the full pipeline

Place retry at the service method level — covering both HTTP fetch and response parsing/validation. The HTTP client should be single-attempt; the service owns retry. Use withRetry from @cyanheads/mcp-ts-core/utils:

typescript
import { withRetry, fetchWithTimeout } from '@cyanheads/mcp-ts-core/utils';
import type { Context } from '@cyanheads/mcp-ts-core';

async fetchItem(id: string, ctx: Context): Promise<Item> {
  return withRetry(
    async () => {
      const response = await fetchWithTimeout(
        `${this.baseUrl}/items/${id}`,
        10_000,
        ctx,
        { signal: ctx.signal },
      );
      const text = await response.text();
      return this.parseResponse<Item>(text);
    },
    {
      operation: 'fetchItem',
      context: ctx,
      baseDelayMs: 1000,    // calibrate to upstream recovery time
      signal: ctx.signal,
    },
  );
}
Key principles
  1. Calibrate backoff to the upstream. 200–500ms for ephemeral failures, 1–2s for rate-limited APIs, 2–5s for service degradation. The default baseDelayMs: 1000 suits most APIs.
  2. Check HTTP status before parsing. fetchWithTimeout already throws on non-OK responses with granular status mapping (401→Unauthorized, 403→Forbidden, 404→NotFound, 408/425→Timeout, 422→ValidationError, 429→RateLimited, 5xx→ServiceUnavailable/InternalError) — this prevents feeding HTML error pages into XML/JSON parsers.
  3. Classify parse failures by content. If the upstream returns HTTP 200 with an HTML error page, detect it and throw ServiceUnavailable (transient) instead of SerializationError (non-transient). Exception — deterministic HTTP 200 errors fail fast, not transient. Some upstreams return HTTP 200 with a structured error body for failures that will never succeed regardless of how many times you retry: a query too expensive for the server's budget, an oversized result set, or a malformed request the server rejects. Retrying these wastes upstream capacity and delays the client. Declare them in the contract with retryable: false (or pass { retryable: false } in data at the throw site) — withRetry's default predicate reads error.data.retryable === false and fails immediately, even for Timeout/ServiceUnavailable codes. ctx.fail auto-populates data.retryable from the contract entry, so declaring it once in errors[] is enough.
  4. Exhausted retries say so. withRetry automatically enriches the final error with attempt count — callers know retries were already attempted.
When you need finer-grained HTTP error classification

fetchWithTimeout already maps status codes to appropriate error codes (see key principle 2 above). Use httpErrorFromResponse instead when you need Retry-After header capture, request body passthrough in error data, or custom service/data fields on the thrown error:

typescript
import { httpErrorFromResponse, withRetry } from '@cyanheads/mcp-ts-core/utils';

async fetchItem(id: string, ctx: Context): Promise<Item> {
  return withRetry(
    async () => {
      const response = await fetch(`${this.baseUrl}/items/${id}`, { signal: ctx.signal });
      if (!response.ok) {
        throw await httpErrorFromResponse(response, {
          service: 'MyAPI',
          data: { itemId: id },
        });
      }
      return this.parseResponse<Item>(await response.text());
    },
    { operation: 'fetchItem', context: ctx, signal: ctx.signal },
  );
}

httpErrorFromResponse maps the full status table (401/403/408/422/429/5xx) to the appropriate JsonRpcErrorCode, captures the response body (truncated), and forwards Retry-After headers into error.data.retryAfter. The codes it produces line up with withRetry's transient-code set, so retryable HTTP failures (429, 503, 504) are retried automatically and non-retryable ones (401, 404, 422) fail immediately.

Response handler pattern
typescript
import { serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';

parseResponse<T>(text: string): T {
  // Detect HTML error pages masquerading as successful responses
  if (/^\s*<(!DOCTYPE\s+html|html[\s>])/i.test(text)) {
    throw serviceUnavailable('API returned HTML instead of expected format — likely rate-limited.');
  }
  // Parse and validate...
}
Sparse upstream payloads

Third-party APIs often omit fields entirely instead of returning null. If your raw response types, normalized domain types, or tool output schemas are stricter than the real upstream payloads, you'll either fail validation or silently invent facts.

Guidance:

  1. Raw upstream types default to optional unless presence is guaranteed. Trust the docs only after you've verified real payloads.
  2. Preserve absence when it means "unknown". Missing data is different from false, 0, '', or an empty array.
  3. Don't fabricate defaults during normalization unless the upstream contract or your own tool semantics explicitly define them.
  4. With exactOptionalPropertyTypes, omit absent fields instead of returning undefined. Conditional spreads keep the normalized object honest.
typescript
type RawRepo = {
  id: string;
  name: string;
  archived?: boolean;
  star_count?: number;
  description?: string | null;
};

type Repo = {
  id: string;
  name: string;
  archived?: boolean;
  starCount?: number;
  description?: string;
};

function normalizeRepo(raw: RawRepo): Repo {
  const description = raw.description?.trim();
  return {
    id: raw.id,
    name: raw.name,
    ...(typeof raw.archived === 'boolean' && { archived: raw.archived }),
    ...(typeof raw.star_count === 'number' && { starCount: raw.star_count }),
    ...(description ? { description } : {}),
  };
}
Show full SKILL.md (629 more words)Show less

Error Handling in Services

Services don't declare errors: [...] contracts and don't have ctx.fail — that contract surface is tool/resource-only. Inside services:

  • Throw via factories when a specific code matters: throw notFound(...), throw rateLimited(...), throw serviceUnavailable(...). The framework's auto-classifier catches anything else.

  • Wrap risky pipelines in ErrorHandler.tryCatch when you want structured logging + auto-classification without writing try/catch boilerplate. It always rethrows — never swallows. Useful for parsing untrusted input (JSON, config) or third-party SDK calls whose error types you don't control:

    ts
    import { ErrorHandler } from '@cyanheads/mcp-ts-core/utils';
    
    const parsed = await ErrorHandler.tryCatch(
      () => JSON.parse(rawConfig),
      { operation: 'MyService.parseConfig', errorCode: JsonRpcErrorCode.ConfigurationError },
    );
  • Tool/resource handlers bubble service errors unchanged — the contract advertises the advertised failure surface, and any code thrown from a service still reaches the client correctly via the auto-classifier. The conformance lint scans handler source text only, so service-thrown codes aren't flagged.

  • Carry contract reason via data: { reason } when the calling tool declares an errors[] contract entry for this failure mode. Services can't call ctx.fail, but passing the reason in data flows through the auto-classifier untouched, so clients see the same error.data.reason they'd see from ctx.fail — and the framework fills that entry's recovery as data.recovery.hint when the throw carries none. No handler-side catch-and-rethrow needed:

    ts
    // tool declares: errors: [{ reason: 'empty_expression', code: JsonRpcErrorCode.ValidationError,
    //                           when: '…', recovery: '…', thrownBy: 'service' }]
    throw validationError('Expression cannot be empty.', { reason: 'empty_expression' });

    The tool's entry carries thrownBy: 'service' so error-contract-unthrown — which reads the handler body and cannot see this throw — skips it while still checking whatever the handler throws itself. Lint-only metadata; nothing at runtime reads it.

  • The contract recovery follows the reason. The calling tool's declared recovery (validated ≥5 words at lint time) is the single source of truth, and the handler factory puts it on the wire for any failure carrying that reason — matched on the reason alone, whatever factory the service picked — so a service throw needs nothing beyond { reason }. For dynamic recovery (interpolating runtime values into the hint), pass an explicit { recovery: { hint: '…' } }, which always wins.

API Efficiency

When a service wraps an external API, design methods to minimize upstream calls. These patterns compound — a tool calling 3 service methods that each make N requests is 3N calls; batching drops it to 3.

Batch over N+1

If the API supports filter-by-IDs, bulk GET, or batch query endpoints, expose a batch method instead of (or alongside) the single-item method. One request for 20 items beats 20 sequential requests — it eliminates serial latency, avoids rate-limit accumulation, and simplifies error handling.

typescript
/** Fetch multiple studies in a single request via filter.ids. */
async getStudiesBatch(nctIds: string[], ctx: Context): Promise<Study[]> {
  const response = await this.searchStudies({
    filterIds: nctIds,
    fields: ['NCTId', 'BriefTitle', 'HasResults', 'ResultsSection'],
    pageSize: nctIds.length,
  }, ctx);
  return response.studies;
}

Cross-reference the response against the requested IDs to detect missing items — don't assume the API returns everything you asked for.

Field selection

If the API supports fields, select, or include parameters, request only what the caller needs. A full record might be 70KB; four fields might be 5KB. Expose field selection as a parameter on the service method, or use sensible defaults per method.

Pagination awareness

If a batch request might exceed the API's page size limit, either:

  • Paginate internally (loop until all pages consumed), or
  • Assert/throw when the response indicates truncation (e.g., nextPageToken present)

Silent truncation is a data integrity bug — the caller thinks it has all results when it doesn't.

Checklist

  • Directory created at src/services/{{domain}}/
  • Service file created — init function accepts (config: AppConfig, storage: StorageService) and stores the instance
  • Accessor function exported — throws Error if not initialized
  • JSDoc @fileoverview and @module header present
  • No console calls — use ctx.log for service-level logging
  • Service methods accept Context for logging and storage
  • init function registered in setup() callback in the server's entry point (src/index.ts or src/worker.ts)
  • If wrapping external API: retry covers full pipeline (fetch + parse), backoff calibrated
  • If wrapping external API: raw/domain types reflect real upstream sparsity; missing values are preserved as unknown, not fabricated into concrete facts
  • If wrapping external API: batch endpoints used where available, field selection applied, pagination handled
  • bun run devcheck passes

© cyanheads, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in framework-skills/add-service of cyanheads/pubmed-mcp-server.

Open the folder on GitHubat commit 79145a6

Compare with similar skills

Add Service next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Add Service compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Add Service this skillcyanheads/pubmed-mcp-server158—~3.6kAutomated safety check: PassApache-2.0
Scientific Brainstormingspacering-net/codeg3.9k13 repos~2kAutomated safety check: PassMIT
MFA Pipeline Orchestratoraiming-lab/AutoResearchClaw15k—~923Automated safety check: PassMIT
Research RefinezjYao36/Auto-Research-Refine1286 repos~6.9kAutomated safety check: NotesNone
Read GitHubAgentTeam-TaichuAI/ScienceClaw6712 repos~638Automated safety check: PassNone
Web ResearchJuncai22/spring-ai-agent-learning1242 repos~1.1kAutomated safety check: PassApache-2.0

Similar skills

  • Scientific Brainstorming

    spacering-net/codeg

    Creative research ideation and exploration. An agent skill from spacering-net/codeg.

    3.9k GitHub starsUsed in 13 repos~2k tokens
    Research & ScienceAuto-check passed
  • MFA Pipeline Orchestrator

    aiming-lab/AutoResearchClaw

    Runs a metabolic flux analysis from model loading to phenotype prediction and figures by handing work to four sub-agents in sequence.

    15k GitHub stars~923 tokensUpdated 1 mo ago
    Research & ScienceAuto-check passed
  • Research Refine

    zjYao36/Auto-Research-Refine

    Turns a vague research direction into a focused, problem-anchored method plan through up to five review rounds with a second model.

    128 GitHub starsUsed in 6 repos~6.9k tokens
    Research & ScienceAuto-check: notes
  • Read GitHub

    AgentTeam-TaichuAI/ScienceClaw

    Read and search GitHub repository documentation via gitmcp.io MCP service.

    671 GitHub starsUsed in 2 repos~638 tokens
    Research & ScienceAuto-check passed
  • Web Research

    Juncai22/spring-ai-agent-learning

    A skill your agent uses for requests related to web research; it provides a structured approach to conducting comprehensive web research

    124 GitHub starsUsed in 2 repos~1.1k tokens
    Research & ScienceAuto-check passed
  • Deepdive

    Socialpranker/deepdive

    Meta-research под вопрос или решение: веб-поиск, источники, Q&A отчёт с цитатами по файлам для повторного использования.

    372 GitHub stars~5.2k tokensUpdated 6 days ago
    Research & ScienceAuto-check passed

More from cyanheads/pubmed-mcp-server

All 30 skills in this repo
  • Add App Tool

    cyanheads/pubmed-mcp-server

    Scaffold an MCP App tool + UI resource pair. An agent skill from cyanheads/pubmed-mcp-server.

    158 GitHub stars~3.2k tokensUpdated today
    Auto-check passed
  • Add Prompt

    cyanheads/pubmed-mcp-server

    Scaffold a new MCP prompt template. An agent skill from cyanheads/pubmed-mcp-server.

    158 GitHub stars~1.6k tokensUpdated today
    Auto-check passed
  • Add Resource

    cyanheads/pubmed-mcp-server

    Scaffold a new MCP resource definition. An agent skill from cyanheads/pubmed-mcp-server.

    158 GitHub stars~3k tokensUpdated today
    Auto-check passed
  • Add Test

    cyanheads/pubmed-mcp-server

    Scaffold a test file for an existing tool, resource, or service.

    158 GitHub stars~4.1k tokensUpdated today
    Auto-check passed
  • API Auth

    cyanheads/pubmed-mcp-server

    Authentication, authorization, and multi-tenancy patterns for @cyanheads/mcp-ts-core.

    158 GitHub stars~2.9k tokensUpdated today
    Auto-check passed
  • API Mirror

    cyanheads/pubmed-mcp-server

    Stand up a persistent, self-refreshing local mirror of a bulk upstream dataset with the MirrorService (@cyanheads/mcp-ts-core/mirror).

    158 GitHub stars~2.6k tokensUpdated today
    Auto-check passed

Questions about Add Service

What does Add Service do?

Scaffold a new service integration. An agent skill from cyanheads/pubmed-mcp-server. Add Service is an agent skill from cyanheads/pubmed-mcp-server. Scaffold a new service integration.

When should I use Add Service?

Add Service fits situations like: the user asks to add a service; integrate an external API; create a reusable domain module with its own initialization and state.

How do I install Add Service in Claude Code?

Run `npx skills add cyanheads/pubmed-mcp-server --skill add-service -a claude-code`. Or copy the skill folder (framework-skills/add-service in cyanheads/pubmed-mcp-server) into .claude/skills/add-service in your project. Claude Code loads it when a task matches its description.

How do I install Add Service in Codex?

Run `npx skills add cyanheads/pubmed-mcp-server --skill add-service -a codex`. Or copy the skill folder (framework-skills/add-service in cyanheads/pubmed-mcp-server) into .agents/skills/add-service in your project. Codex loads it when a task matches its description.

Can I use Add Service in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add cyanheads/pubmed-mcp-server --skill add-service -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/add-service, .gemini/skills/add-service, .github/skills/add-service and .opencode/skills/add-service in your project.

What does Add Service need to run?

Going by SKILL.md and its folder, Add Service needs the command-line tools its instructions call (bun).

Does Add Service access the network?

SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.

Is Add Service safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Add Service use?

Add Service is published under the Apache-2.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Add Service use?

About 3.6k tokens (SKILL.md is roughly 15k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Add Service?

Skills that share tags, products or a category with Add Service: Scientific Brainstorming (spacering-net/codeg, 3.9k stars), MFA Pipeline Orchestrator (aiming-lab/AutoResearchClaw, 15k stars), Research Refine (zjYao36/Auto-Research-Refine, 128 stars) and Read GitHub (AgentTeam-TaichuAI/ScienceClaw, 671 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Add Service?

cyanheads (a GitHub user) maintains it in cyanheads/pubmed-mcp-server, which has 158 GitHub stars. The repository holds 30 skills in this directory. The repository was last updated on October 9, 2026.

Source: cyanheads/pubmed-mcp-server on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.