This plan focuses on simplifying skill-creator while keeping the current runtime behavior: dynamic CLIs for OpenAPI, MCP stdio, MCP HTTP/SSE, and GraphQL.
- Reduce custom parsing and protocol glue code.
- Keep the CLI dynamic and lightweight.
- Prefer stable, focused libraries over large frameworks.
- Preserve current behavior and test coverage during each step.
- Avoid broad rewrites; refactor in small, independently testable phases.
src/cli/main.tsis the largest file and mixes parsing, dispatch, command rendering, mode handling, stdin, cache setup, and execution.- CLI parsing is custom even though Node 20 has
util.parseArgs. - OpenAPI local
$refresolution is custom despite an existing dependency that can dereference schemas. - MCP stdio command splitting is custom shell-like parsing.
- GraphQL execution manually handles request serialization, HTTP status checks, JSON parsing, and GraphQL error formatting.
- Mode handlers repeat the same flow: load commands, filter/search, list, find subcommand, render help, parse values, execute, format output.
- Keep public CLI behavior stable.
- Refactor one concern at a time.
- Add or update tests before each behavior change.
- Prefer removing code over adding abstraction unless repetition is clear.
- Avoid heavyweight CLI frameworks because commands are discovered at runtime.
Create a shared helper for the common command lifecycle.
Repeated flow today:
- load commands
- apply include/exclude/method filters
- apply search
- render list/search results
- validate subcommand
- render command help
- parse command values
- execute command
- format output
Target shape:
await runDynamicMode({
globals,
commandArgv,
loadCommands,
renderCommands,
executeCommand,
});Expected impact:
- Shrinks
src/cli/main.tssignificantly. - Makes OpenAPI, GraphQL, MCP HTTP, and MCP stdio behavior more consistent.
- Makes future modes easier to add.
Risk: low to medium. This is mostly internal structure, but it touches all modes.
Validation:
pnpm typecheck
pnpm test
pnpm lint
pnpm fmt:checkUse Node's built-in parser before adding a third-party CLI framework.
Benefits:
- No new dependency.
- Supports typed option specs.
- Handles
--flag=valueand positional tokens. - Fits Node 20+ runtime requirement.
Likely replacements:
parseGlobalArgs- parts of
parseCommandValues splitOptionValuereadOptionValue
Keep splitAtSubcommand only if still needed for dynamic command boundaries. Otherwise, replace it too.
Possible shape:
import { parseArgs } from 'node:util';
const parsed = parseArgs({
args: argv,
options: {
spec: { type: 'string' },
mcp: { type: 'string' },
'mcp-stdio': { type: 'string' },
graphql: { type: 'string' },
list: { type: 'boolean' },
pretty: { type: 'boolean' },
},
allowPositionals: true,
});Third-party alternative:
pnpm add commandercommander is acceptable if built-in parsing becomes awkward, but it is less natural for runtime-discovered commands. Avoid larger frameworks unless the CLI becomes much more complex.
Risk: medium. Parsing changes can subtly affect user-facing behavior.
Validation focus:
--flag value--flag=value- repeated
--auth-header - leading
--from package-manager invocation - command-specific flags
- unknown command flags
- boolean parameters
Current custom file:
src/openapi/refs.ts
The project already depends on:
"@apidevtools/json-schema-ref-parser"Use it directly or remove the dependency if we decide to keep custom logic. Preferred option:
import $RefParser from '@apidevtools/json-schema-ref-parser';
const spec = await $RefParser.dereference(parsed);Benefits:
- Deletes custom
$refresolver code. - Handles more reference edge cases.
- Reduces maintenance burden.
Optional OpenAPI-specific alternative:
pnpm add @apidevtools/swagger-parserUse @apidevtools/swagger-parser if we want stronger OpenAPI validation/parsing later.
Risk: medium. Dereferencing behavior may differ for circular references or remote refs.
Validation focus:
- local
$ref - nested schemas
- circular refs if supported or intentionally rejected
- existing OpenAPI tests
Current custom logic:
splitCommandLineinsrc/mcp/stdio.ts
Recommended dependency:
pnpm add string-argvTarget:
import stringArgv from 'string-argv';
const [command, ...args] = stringArgv(commandLine);Benefits:
- Removes custom shell-ish quote/escape parsing.
- Better tested behavior for quoted command lines.
- Smaller MCP stdio module.
Risk: low to medium. Shell parsing compatibility can change slightly.
Validation focus:
- quoted args
- escaped quotes
- paths with spaces
- empty command error
- filesystem MCP smoke command
Current custom logic in src/graphql/execute.ts:
- manual
fetch - manual JSON payload creation
- manual response parsing
- manual GraphQL error formatting
Recommended dependency:
pnpm add graphql-requestTarget shape:
import { GraphQLClient } from 'graphql-request';
const client = new GraphQLClient(endpoint, {
headers: Object.fromEntries(authHeaders),
});
const data = await client.request<Record<string, unknown>>(query, variables);
return data[fieldName];Benefits:
- Removes request/error boilerplate.
- Uses a focused, common GraphQL client.
- Keeps schema extraction and query generation under our control.
Keep custom:
- command extraction from schema
- variable collection
- default field selection logic
Risk: low to medium. Error messages may change.
Validation focus:
- successful query
- GraphQL error response
- HTTP error response
- auth headers
- variables
--fields--selection-depth--stdin
Current schema loading in src/graphql/load.ts handles:
- endpoint introspection
- SDL files
- introspection JSON files
- schema URLs
- stale cache fallback
Optional dependencies:
pnpm add @graphql-tools/load @graphql-tools/url-loader @graphql-tools/graphql-file-loader @graphql-tools/json-file-loaderPossible target:
import { loadSchema } from '@graphql-tools/load';
import { UrlLoader } from '@graphql-tools/url-loader';
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
import { JsonFileLoader } from '@graphql-tools/json-file-loader';
const schema = await loadSchema(source, {
loaders: [new UrlLoader(), new GraphQLFileLoader(), new JsonFileLoader()],
headers: Object.fromEntries(authHeaders),
});Benefits:
- More robust schema loading.
- Better support for schema URLs/files.
- Less custom introspection/file parsing code.
Reasons to delay:
- Adds multiple dependencies.
- Current loader is not huge.
- We need to preserve custom stale-cache behavior.
Recommendation: do this only after graphql-request, and only if schema loading becomes a maintenance problem.
Risk: medium.
Current query generation manually concatenates strings.
Use the existing graphql package to build an AST and print it:
import { print, Kind, type DocumentNode } from 'graphql';Benefits:
- Reduces invalid query risks.
- Gives safer operation names, variable definitions, arguments, and nested selections.
- Makes future GraphQL features easier.
Tradeoff:
- May not reduce line count.
- Safer, but more verbose.
Recommendation: optional. Do this if GraphQL query generation gets more complex.
Risk: medium.
Current coercion is intentionally small:
src/core/coerce.ts
Recommended dependencies:
pnpm add ajv ajv-formatsUse cases:
- validate MCP tool args against input schemas
- validate OpenAPI params and request bodies
- provide better errors for invalid input
- eventually reduce custom coercion rules
Benefits:
- Better correctness.
- Better user-facing validation errors.
- Uses schemas already provided by MCP/OpenAPI.
Reasons to delay:
- Ajv adds complexity.
- Coercion semantics need careful design for CLI strings.
- Validation errors can be verbose unless formatted well.
Recommendation: add after parser/refactoring work, not first.
Risk: medium to high depending on how strict validation becomes.
Current HTTP usage relies on built-in fetch, which is fine on Node 20+.
Optional dependency:
pnpm add kyPotential benefits:
- timeout support
- retries
- cleaner JSON handling
- hooks for auth headers
Reasons to avoid for now:
- Built-in
fetchis adequate. - Most code is protocol-specific, not generic HTTP boilerplate.
- Adding
kymay not delete much code.
Recommendation: skip unless we need retries/timeouts globally.
Current cache module is small:
src/core/cache.ts
Optional dependencies:
pnpm add cacachePotential benefits:
- robust content-addressable cache
- integrity handling
- battle-tested npm-style cache behavior
Reasons to avoid for now:
- Current cache is simple and understandable.
- TTL behavior is project-specific.
- A cache dependency may add more complexity than it removes.
Recommendation: keep current cache until requirements grow.
High-value additions:
pnpm add string-argv graphql-requestUse existing dependency:
@apidevtools/json-schema-ref-parserLater, if needed:
pnpm add ajv ajv-formats
pnpm add @graphql-tools/load @graphql-tools/url-loader @graphql-tools/graphql-file-loader @graphql-tools/json-file-loaderProbably avoid for now:
pnpm add commander
pnpm add ky
pnpm add cacache- Add a shared dynamic-mode runner.
- Replace OpenAPI
$refresolver with the existing ref parser dependency. - Replace MCP stdio command splitting with
string-argv. - Replace GraphQL execution transport with
graphql-request. - Replace CLI parsing with Node
util.parseArgs. - Consider Ajv validation.
- Consider GraphQL Tools schema loading.
- Consider GraphQL AST query generation.
This order minimizes risk because it first removes duplication, then replaces isolated custom utilities, and only later changes parser behavior.
For each phase:
pnpm typecheck
pnpm test
pnpm lint
pnpm fmt:check
pnpm buildImportant smoke checks:
node dist/cli/main.js --version
node dist/cli/main.js --help
node dist/cli/main.js --graphql https://beta.pokeapi.co/graphql/v1beta commands listFor MCP stdio after string-argv:
pnpm dev -- --mcp-stdio \
"npx -y -p ajv -p @modelcontextprotocol/server-filesystem mcp-server-filesystem /tmp" \
commands list- Existing tests still pass.
- CLI examples still work.
src/cli/main.tsis smaller and mostly dispatch-oriented.- Custom parser/ref/schema/request helpers are removed where focused libraries are better.
- No large framework controls the dynamic CLI architecture.