
Research
/Security News
CanisterWorm: npm Publisher Compromise Deploys Backdoor Across 29+ Packages
The worm-enabled campaign hit @emilgroup and @teale.io, then used an ICP canister to deliver follow-on payloads.
@mcp-apps-kit/core
Advanced tools
Server-side framework for building MCP applications.
MCP AppsKit Core is the server runtime for defining tools, validating inputs and outputs with Zod, and binding UI resources. It targets both MCP Apps and ChatGPT (OpenAI Apps SDK) from the same definitions.
Interactive MCP apps often need to support multiple hosts with slightly different APIs and metadata rules. Core provides a single server-side API for tools, metadata, and UI resources so you can support MCP Apps and ChatGPT Apps without parallel codebases.
createApp() entry point for tools and UI definitions/v1/mcp, /v2/mcp)>= 18^4.0.0 (peer dependency)@modelcontextprotocol/sdknpm install @mcp-apps-kit/core zod
import { createApp, defineTool } from "@mcp-apps-kit/core";
import { z } from "zod";
const app = createApp({
name: "my-app",
version: "1.0.0",
tools: {
greet: defineTool({
description: "Greet a user",
input: z.object({
name: z.string().describe("Name to greet"),
}),
output: z.object({ message: z.string() }),
handler: async (input) => {
return { message: `Hello, ${input.name}!` };
},
}),
},
});
await app.start({ port: 3000 });
Tools can include a UI definition for displaying results. Use defineUI for type-safe UI definitions, then reference it from your tool. Return UI-only payloads in _meta.
import { createApp, defineTool, defineUI } from "@mcp-apps-kit/core";
import { z } from "zod";
// Define UI widget for displaying restaurant list
const restaurantListUI = defineUI({
name: "Restaurant List",
description: "Displays restaurant search results",
html: "./dist/widget.html",
prefersBorder: true,
autoResize: true, // Enable automatic size notifications (default: true, MCP Apps only)
});
const app = createApp({
name: "restaurant-finder",
version: "1.0.0",
tools: {
search_restaurants: defineTool({
description: "Search for restaurants by location",
input: z.object({ location: z.string() }),
output: z.object({ count: z.number() }),
handler: async ({ location }) => {
const restaurants = await fetchRestaurants(location);
return {
count: restaurants.length,
_meta: { restaurants },
};
},
ui: restaurantListUI,
}),
},
});
defineTool helperUse defineTool to get automatic type inference in your handlers:
import { defineTool } from "@mcp-apps-kit/core";
tools: {
search: defineTool({
input: z.object({
query: z.string(),
maxResults: z.number().optional(),
}),
handler: async (input) => {
return { results: await search(input.query, input.maxResults) };
},
}),
}
Why defineTool?
With Zod v4, TypeScript cannot infer concrete schema types across module boundaries when using generic z.ZodType. The defineTool helper captures specific schema types at the call site, enabling proper type inference without manual type assertions.
const searchInput = z.object({
query: z.string(),
maxResults: z.number().optional(),
});
const app = createApp({
tools: {
search: {
input: searchInput,
handler: async (input) => {
const typed = input as z.infer<typeof searchInput>;
return { results: await search(typed.query, typed.maxResults) };
},
},
},
});
ClientToolsFromCoreExport types from your server to use in UI code for fully typed tool results:
// server/index.ts
import { createApp, defineTool, type ClientToolsFromCore } from "@mcp-apps-kit/core";
import { z } from "zod";
const app = createApp({
name: "my-app",
version: "1.0.0",
tools: {
greet: defineTool({
description: "Greet a user",
input: z.object({ name: z.string() }),
output: z.object({ message: z.string(), timestamp: z.string() }),
handler: async (input) => ({
message: `Hello, ${input.name}!`,
timestamp: new Date().toISOString(),
}),
}),
},
});
// Export types for UI code
export type AppTools = typeof app.tools;
export type AppClientTools = ClientToolsFromCore<AppTools>;
Then in your UI code, use the exported types with React hooks:
// ui/Widget.tsx
import { useToolResult } from "@mcp-apps-kit/ui-react";
import type { AppClientTools } from "../server";
function Widget() {
// Fully typed: result?.greet?.message is typed as string | undefined
const result = useToolResult<AppClientTools>();
if (result?.greet) {
return (
<p>
{result.greet.message} at {result.greet.timestamp}
</p>
);
}
return <p>Waiting for greeting...</p>;
}
Expose multiple API versions from a single application, each with its own tools, UI, and optional configuration overrides.
const app = createApp({
name: "my-app",
// Shared config across all versions
config: {
cors: { origin: true },
debug: { logTool: true, level: "info" },
},
// Version definitions
versions: {
v1: {
version: "1.0.0",
tools: {
greet: defineTool({
description: "Greet v1",
input: z.object({ name: z.string() }),
output: z.object({ message: z.string() }),
handler: async ({ name }) => ({ message: `Hello, ${name}!` }),
}),
},
},
v2: {
version: "2.0.0",
tools: {
greet: defineTool({
description: "Greet v2",
input: z.object({ name: z.string(), surname: z.string().optional() }),
output: z.object({ message: z.string() }),
handler: async ({ name, surname }) => ({
message: `Hello, ${name} ${surname || ""}!`.trim(),
}),
}),
},
},
},
});
await app.start({ port: 3000 });
// Each version is exposed at its dedicated route:
// - v1: http://localhost:3000/v1/mcp
// - v2: http://localhost:3000/v2/mcp
Version-specific configs are merged with global config, with version-specific taking precedence:
const app = createApp({
name: "my-app",
config: {
cors: { origin: true },
oauth: { authorizationServer: "https://auth.example.com" },
},
versions: {
v1: {
version: "1.0.0",
tools: {
/* ... */
},
// Uses global OAuth config
},
v2: {
version: "2.0.0",
tools: {
/* ... */
},
config: {
// Override OAuth for v2
oauth: { authorizationServer: "https://auth-v2.example.com" },
// Override protocol
protocol: "openai",
},
},
},
});
Plugins are merged: global plugins apply to all versions, version-specific plugins are added per version:
const globalPlugin = createPlugin({
name: "global-logger",
onInit: () => console.log("App initializing"),
});
const v2Plugin = createPlugin({
name: "v2-analytics",
beforeToolCall: (context) => {
if (context.toolName === "greet") {
analytics.track("v2_greet_called");
}
},
});
const app = createApp({
name: "my-app",
plugins: [globalPlugin], // Applied to all versions
versions: {
v1: {
version: "1.0.0",
tools: {
/* ... */
},
},
v2: {
version: "2.0.0",
tools: {
/* ... */
},
plugins: [v2Plugin], // Only applied to v2
},
},
});
Each version can have its own middleware chain:
const app = createApp({
name: "my-app",
versions: {
v1: {
version: "1.0.0",
tools: {
/* ... */
},
},
v2: {
version: "2.0.0",
tools: {
/* ... */
},
},
},
});
// Add middleware to specific version
const v2App = app.getVersion("v2");
v2App?.use(async (context, next) => {
console.log("v2 middleware");
await next();
});
// Get list of available version keys
const versions = app.getVersions(); // ["v1", "v2"]
// Get a specific version app instance
const v1App = app.getVersion("v1");
const v2App = app.getVersion("v2");
// Access version-specific tools, middleware, etc.
if (v2App) {
v2App.use(v2SpecificMiddleware);
}
Version keys must match the pattern /^v\d+$/ (e.g., v1, v2, v10):
versions: {
v1: { /* ... */ }, // ✅ Valid
v2: { /* ... */ }, // ✅ Valid
v10: { /* ... */ }, // ✅ Valid
"v1.0": { /* ... */ }, // ❌ Invalid (must be v1, v2, etc.)
"beta": { /* ... */ }, // ❌ Invalid
}
All versions share:
GET /health (returns all available versions)GET /.well-known/openai-apps-challenge (if configured)Single-version apps continue to work as before:
// Single-version (backward compatible)
const app = createApp({
name: "my-app",
version: "1.0.0",
tools: {
/* ... */
},
});
// getVersions() returns empty array for single-version apps
app.getVersions(); // []
// getVersion() returns undefined for single-version apps
app.getVersion("v1"); // undefined
import { createPlugin } from "@mcp-apps-kit/core";
const loggingPlugin = createPlugin({
name: "logger",
version: "1.0.0",
onInit: async () => console.log("App initializing..."),
onStart: async () => console.log("App started"),
beforeToolCall: async (context) => {
console.log(`Tool called: ${context.toolName}`);
},
afterToolCall: async (context) => {
console.log(`Tool completed: ${context.toolName}`);
},
});
import type { Middleware } from "@mcp-apps-kit/core";
const logger: Middleware = async (context, next) => {
const start = Date.now();
context.state.set("startTime", start);
await next();
console.log(`${context.toolName} completed in ${Date.now() - start}ms`);
};
app.use(logger);
app.on("tool:called", ({ toolName }) => {
analytics.track("tool_called", { tool: toolName });
});
Enable debug logging to receive structured logs from client UIs through the MCP protocol.
const app = createApp({
name: "my-app",
version: "1.0.0",
tools: {
/* ... */
},
config: {
debug: {
logTool: true,
level: "debug",
},
},
});
You can also use the server-side logger directly:
import { debugLogger } from "@mcp-apps-kit/core";
debugLogger.info("User logged in", { userId: "456" });
Core validates bearer tokens and injects auth metadata for tool handlers.
const app = createApp({
name: "my-app",
version: "1.0.0",
tools: {
/* ... */
},
config: {
oauth: {
protectedResource: "http://localhost:3000",
authorizationServer: "https://auth.example.com",
scopes: ["mcp:read", "mcp:write"],
},
},
});
OAuth metadata is injected into _meta and surfaced via context.subject and context.raw:
tools: {
get_user_data: defineTool({
description: "Get authenticated user data",
input: z.object({}),
handler: async (_input, context) => {
const subject = context.subject;
const auth = context.raw?.["mcp-apps-kit/auth"] as
| {
subject: string;
scopes: string[];
expiresAt: number;
clientId: string;
issuer: string;
audience: string | string[];
token?: string;
extra?: Record<string, unknown>;
}
| undefined;
return { userId: subject, scopes: auth?.scopes ?? [] };
},
}),
}
When OAuth is enabled, the server exposes:
/.well-known/oauth-authorization-server/.well-known/oauth-protected-resourceThese endpoints describe the external authorization server and this protected resource. They do not issue tokens.
For non-JWT tokens or token introspection:
import type { TokenVerifier } from "@mcp-apps-kit/core";
const customVerifier: TokenVerifier = {
async verifyAccessToken(token: string) {
const response = await fetch("https://auth.example.com/introspect", {
method: "POST",
body: new URLSearchParams({ token }),
});
const data = await response.json();
if (!data.active) {
throw new Error("Token inactive");
}
return {
token,
clientId: data.client_id,
scopes: data.scope.split(" "),
expiresAt: data.exp,
extra: { subject: data.sub },
};
},
};
const app = createApp({
name: "my-app",
version: "1.0.0",
tools: {
/* ... */
},
config: {
oauth: {
protectedResource: "http://localhost:3000",
authorizationServer: "https://auth.example.com",
tokenVerifier: customVerifier,
},
},
});
iss, aud, exp, sub, client_idconst app = createApp({
name: "my-app",
version: "1.0.0",
tools: {
/* ... */
},
});
When submitting your app to the ChatGPT App Store, OpenAI requires domain verification to confirm ownership of the MCP server host.
const app = createApp({
name: "my-app",
version: "1.0.0",
tools: {
/* ... */
},
config: {
openai: {
domain_challenge: "your-verification-token-from-openai",
},
},
});
config.openai.domain_challengeGET /.well-known/openai-apps-challenge returning the token as plain texttext/plain as requiredhandleRequest)../../examples/minimal - demonstrates API versioning with v1 and v2 endpoints../../examples/restaurant-finder - end-to-end app with search functionalityKey exports include:
createApp, defineTool, defineUIcreatePlugin, loggingPlugindebugLogger, ClientToolsFromCoreMiddleware, TypedEventEmitterFor full types, see packages/core/src/types or the project overview in ../../README.md.
See ../../CONTRIBUTING.md for development setup and guidelines. Issues and pull requests are welcome.
MIT
FAQs
Server-side framework for building MCP applications
The npm package @mcp-apps-kit/core receives a total of 12 weekly downloads. As such, @mcp-apps-kit/core popularity was classified as not popular.
We found that @mcp-apps-kit/core demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
The worm-enabled campaign hit @emilgroup and @teale.io, then used an ICP canister to deliver follow-on payloads.

Research
/Security News
Attackers compromised Trivy GitHub Actions by force-updating tags to deliver malware, exposing CI/CD secrets across affected pipelines.

Security News
ENISA’s new package manager advisory outlines the dependency security practices companies will need to demonstrate as the EU’s Cyber Resilience Act begins enforcing software supply chain requirements.