Build a TypeScript MCP server that calls CometAPI

Last reviewed: 2026-08-09

Direct answer

A TypeScript MCP server can expose a small, model-controlled tool and keep the CometAPI call behind that tool boundary. Create an McpServer, register a tool with a JSON Schema input, instantiate the official OpenAI-compatible Node client with a base URL supplied by configuration, call chat.completions.create, and return a short text result. Keep credentials in the process environment, validate tool input before the network call, and return an MCP error result when the upstream request fails.

The current CometAPI quickstart describes CometAPI as an OpenAI-compatible endpoint that can be used with standard Python and Node.js SDKs. The MCP tools specification defines a tool as a named operation with metadata and an input schema, and its example uses tools/call to invoke that operation. The official MCP TypeScript SDK README shows the v2 McpServer, registerTool, Zod schema, and StdioServerTransport pattern. Together, those contracts give the server two clean boundaries: MCP validates the incoming tool arguments, while the OpenAI-compatible client sends the model request.

Install the server package, its stdio transport, the OpenAI-compatible client, and Zod:

npm install @modelcontextprotocol/server openai zod

Supply the CometAPI credential through the standard process-environment configuration read by the OpenAI Node client, and set COMETAPI_BASE_URL to the current endpoint shown in the CometAPI quickstart . Keeping the endpoint in configuration makes changes reviewable and keeps URLs out of source code. The following example accepts a model name from the caller. In production, replace that open string with an allow-list checked against the current CometAPI catalog.

import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
import OpenAI from 'openai';
import * as z from 'zod/v4';

const configuredBaseURL = process.env.COMETAPI_BASE_URL;
if (!configuredBaseURL) {
  throw new Error('CometAPI endpoint configuration is missing');
}

const modelClient = new OpenAI({
  baseURL: configuredBaseURL,
});

const server = new McpServer({
  name: 'cometapi-summary-server',
  version: '1.0.0',
});

server.registerTool(
  'summarize_text',
  {
    description: 'Summarize supplied text with CometAPI',
    inputSchema: z.object({
      text: z.string().min(1).max(12000),
      model: z.string().min(1).max(120),
    }),
  },
  async ({ text, model }) => {
    const startedAt = Date.now();

    try {
      const completion = await modelClient.chat.completions.create({
        model,
        messages: [
          {
            role: 'user',
            content: 'Summarize the following text in five bullet points: ' + text,
          },
        ],
      });

      const answer = completion.choices[0]?.message?.content;
      if (typeof answer !== 'string' || answer.length === 0) {
        console.error(JSON.stringify({
          event: 'mcp_tool_completed',
          tool: 'summarize_text',
          model,
          outcome: 'empty_result',
          duration_ms: Date.now() - startedAt,
        }));

        return {
          isError: true,
          content: [{ type: 'text', text: 'CometAPI returned no text content' }],
        };
      }

      console.error(JSON.stringify({
        event: 'mcp_tool_completed',
        tool: 'summarize_text',
        model,
        outcome: 'success',
        duration_ms: Date.now() - startedAt,
      }));

      return {
        content: [{ type: 'text', text: answer }],
      };
    } catch (error) {
      console.error(JSON.stringify({
        event: 'mcp_tool_completed',
        tool: 'summarize_text',
        model,
        outcome: 'error',
        duration_ms: Date.now() - startedAt,
        error_type: error instanceof Error ? error.name : 'unknown',
      }));

      return {
        isError: true,
        content: [{ type: 'text', text: 'The CometAPI request failed' }],
      };
    }
  },
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((error) => {
  console.error(JSON.stringify({
    event: 'server_start_failed',
    error_type: error instanceof Error ? error.name : 'unknown',
  }));
  process.exitCode = 1;
});

The OpenAI Node library documents the TypeScript client and the chat-completion call used here. It also shows that the client reads its credential from the process environment by default. That lets the example avoid embedding, assigning, or logging any credential value.

The tool accepts a model name so an MCP host can select from an approved catalog. A production server should usually constrain that field to a local allow-list. That is an application policy rather than a claim that every catalog model supports every optional request parameter.

Who this is for

This pattern is for TypeScript developers who already have an MCP-capable host and want one narrowly scoped tool to call a model gateway. It fits a local stdio server used by a desktop assistant, development agent, or test harness. It is also a useful starting point for a remote server, although remote deployment adds transport, authorization, origin, and concurrency decisions that are outside this minimal example.

It is not a replacement for a full agent framework. The tool does one job, accepts bounded text, calls one upstream endpoint, and returns text. That narrow contract makes it easier to review permissions and observe failures. If you need graph orchestration, compare the existing LangGraph integration tutorial separately from this MCP boundary.

Key takeaways

  • Treat an MCP tool as a capability boundary, not as a pass-through for arbitrary upstream parameters.
  • Use the SDK’s Zod-backed input schema to reject empty or oversized text before making a request.
  • Supply the CometAPI credential through process configuration; never print it, place it in a URL, or include it in an MCP argument.
  • Write stdio-server diagnostics to stderr so they do not mix with protocol output.
  • Log only fields such as event, tool, model, outcome, duration_ms, and error_type; omit prompts, completions, headers, and environment contents.
  • Pin or allow-list model names when reproducibility matters, and verify the selected model in the current catalog.
  • Exercise the tool through the host’s normal discovery and invocation flow before expanding its permissions.

Sources checked

The CometAPI quickstart was checked for the OpenAI-compatible endpoint and standard-SDK configuration. The MCP server-tools specification was checked for tool metadata, input schemas, tools/list, tools/call, result content, and the isError result flag. The MCP TypeScript SDK repository was checked for the current v2 package names, supported runtimes, Zod-compatible schemas, and minimal stdio server registration. The OpenAI Node library was checked for the TypeScript client, chat-completion method, process-environment configuration, and configurable baseURL option.

These sources are complementary: CometAPI documents gateway configuration, MCP defines the tool protocol, the MCP SDK supplies the server implementation, and the OpenAI library supplies the upstream client. None of them establishes that a particular model supports every optional request field, so the example keeps the request intentionally small.

Contract details to verify

Before deploying the server, verify these boundaries against the current sources and your chosen model:

  1. Tool schema. The MCP specification requires a valid JSON Schema object for inputSchema. Keep the schema deterministic and make its limits visible to callers. The SDK example uses Zod, which is the approach used here.
  2. Tool result. Return an array of content items with a text item for successful output. Use isError: true for an execution failure so the host can distinguish a failed call from a successful answer containing ordinary text.
  3. Client endpoint. Confirm that COMETAPI_BASE_URL points to the current OpenAI-compatible endpoint, and test a harmless request with a model the account can access. Do not assume a model identifier copied from an old snippet remains active.
  4. Credential boundary. Let the runtime provide the credential through the client library’s documented environment mechanism. Do not accept it as a tool argument, mirror it into MCP headers, include it in process output, or place it in a URL.
  5. Transport. Stdio is appropriate when the host launches the process. For remote deployment, select an SDK transport and hosting adapter that match the runtime, then review authorization and request-boundary behavior separately.
  6. User control. The MCP tools guidance recommends clear indicators and a human ability to deny tool invocations. Expose only the operation and data scope the host actually needs.

For a second implementation perspective, review the existing OpenAI-compatible gateway checklist before release. It covers adjacent client and documentation checks without changing this article’s MCP-specific contract.

Failure modes

Missing runtime configuration. Treat a missing endpoint or client credential as a startup failure. Starting a server that cannot make any upstream call creates noisy, repeated failures. A supervisor should expose the startup state without printing environment values or a complete environment dump.

Invalid tool arguments. Empty text, overlong text, or an empty model name should be rejected by the schema before a network request begins. If the host reports a schema error, fix the caller or its prompt rather than retrying the same payload.

Unknown or unavailable model. A model can be misspelled, removed from an allow-list, or unavailable to the account. Log the model identifier and the sanitized outcome, return a generic message to the user, and consult the current catalog before changing code.

Rate limit, connectivity failure, or upstream rejection. Any of these can leave the tool without an answer. Keep the user-facing result stable. Add a bounded timeout and retry policy only after reviewing current endpoint guidance, and avoid automatic retries when request acceptance is uncertain.

Unexpected response shape. A completed request can still contain no usable message content. The explicit string check converts that condition into an MCP error instead of passing an empty value into the host.

Protocol or transport mismatch. A host may expect a different MCP SDK generation or transport. Confirm the SDK version and transport together, run tool discovery, and then call the tool with a small fixture. In a stdio server, keep diagnostics on stderr so logs do not interfere with protocol messages.

Sensitive logging. Logging the input text, generated answer, environment object, request headers, or raw upstream error can disclose user or credential material. The sample logs only operation metadata. If an error library exposes a numeric status, add it only after confirming the serialization excludes request headers and bodies.

Use this concrete happy-path and error-path workflow:

1. Supply the endpoint and client credential through the process environment.
2. Start the server and confirm that startup emits no sensitive values.
3. Ask the MCP host to list tools and confirm summarize_text appears with its schema.
4. Call summarize_text with a short, non-sensitive fixture and an approved model.
5. Confirm the host receives one text content item and the log outcome is success.
6. Call the tool with empty text and confirm schema validation rejects it before an upstream call.
7. Use a deliberately unavailable model and confirm the host receives isError=true.
8. Simulate a connectivity failure and confirm the result remains generic.
9. Inspect stderr for event, tool, model, outcome, duration_ms, and error_type only.
10. Confirm the prompt, completion, headers, credential, and environment contents are absent.

A successful run proves discovery, validation, upstream invocation, result mapping, and sanitized observability. The error checks prove that invalid input and upstream failures do not turn into leaked implementation details.

FAQ

Can I use the OpenAI Node client with CometAPI?

The checked CometAPI quickstart and OpenAI Node documentation support the compatible-client pattern: configure the client for CometAPI’s endpoint and use the standard chat-completion method. Confirm the current endpoint and model catalog before shipping.

Should the tool accept arbitrary model parameters?

Usually no. Start with a small schema and fixed request shape. Every additional parameter expands the compatibility surface and gives callers more ways to reach provider-specific behavior. Add fields only after testing them with the models you intend to support.

Why return a generic error instead of the upstream body?

Upstream errors may contain request details, internal identifiers, headers, or sensitive material. A stable public message protects the user boundary, while a sanitized structured log gives operators enough information to classify the failure. Preserve a request correlation identifier only if the runtime already supplies one and it is safe to record.

Can I expose this over HTTP?

Yes, but the example is intentionally stdio-based. An HTTP server needs a deliberate transport, authorization policy, origin policy, timeout policy, and concurrency model. Treat those as a separate deployment design and verify them against the MCP version the host supports.

Why is the model name an input?

It keeps the tutorial independent of a fast-changing catalog. For production, an enum or server-side mapping is safer because it prevents callers from selecting an unreviewed model. Keep the tool name and schema stable even if the backing model changes.

What should I test first?

Test discovery, one known-good call, schema rejection, an unavailable model, and a simulated connectivity failure. Keep the fixture short and non-sensitive. Repeat those checks whenever you change the SDK, transport, model allow-list, or gateway configuration.

Reader next step

Create a small local project, install the packages shown above, and place the server in a TypeScript entry point. Supply the endpoint and credential only through runtime configuration. Run the ten-step happy/error-path workflow and retain only the sanitized operational fields.

When that works, read the CometAPI prompt evaluation tutorial to turn the same non-sensitive fixture into repeatable assertions. The TypeScript request typing guide is the next useful reference if you want to tighten the boundary around the upstream request.

Start with one read-only summarization capability, review what the host displays before approving calls, and expand the server only when each new tool has an explicit schema, bounded input, and tested failure behavior.