Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .chronus/changes/move-typespec-mcp-2025-10-26-18-56-53.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: internal
packages:
- "@typespec/mcp"
---

Migrate typespec-mcp into `@typespec/mcp`
21 changes: 21 additions & 0 deletions packages/mcp/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright 2025 (c) Microsoft Corporation.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
3 changes: 3 additions & 0 deletions packages/mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @typespec/mcp

TypeSpec library providing decorators for the Mode[l Context Protocol(MCP)](https://modelcontextprotocol.io/).
11 changes: 11 additions & 0 deletions packages/mcp/generated-defs/TypeSpec.MCP.Private.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { DecoratorContext, Model, Type } from "@typespec/compiler";

export type SerializeAsTextDecorator = (
context: DecoratorContext,
target: Model,
type: Type,
) => void;

export type TypeSpecMCPPrivateDecorators = {
serializeAsText: SerializeAsTextDecorator;
};
84 changes: 84 additions & 0 deletions packages/mcp/generated-defs/TypeSpec.MCP.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { DecoratorContext, Interface, Namespace, Operation, Type } from "@typespec/compiler";

export interface McpServerOptions {
readonly name?: string;
readonly version?: string;
readonly instructions?: string;
}

/**
* Declare an operation that is an MCP Tool.
*
* @param value Default is true.
*/
export type ToolDecorator = (context: DecoratorContext, target: Operation) => void;

/**
* Tool does not modify its environment.
*
* @param value Default is true.
*/
export type ReadonlyDecorator = (
context: DecoratorContext,
target: Operation,
value?: Type,
) => void;

/**
* Tool will not perform any destructive operations.
*
* @param value Default is true.
*/
export type NondestructiveDecorator = (
context: DecoratorContext,
target: Operation,
value?: Type,
) => void;

/**
* Repeated calls with same args have no additional effect
*
* @param value Default is true.
*/
export type IdempotentDecorator = (
context: DecoratorContext,
target: Operation,
value?: Type,
) => void;

/**
* Tool will not interacts with external entities
*
* @param value Default is true.
*/
export type ClosedWorldDecorator = (
context: DecoratorContext,
target: Operation,
value?: Type,
) => void;

export type ResourceDecorator = (
context: DecoratorContext,
target: Operation,
uri?: string,
) => void;

/**
* Declare a namespace or interface as an MCP Server and provide server
* metadata.
*/
export type McpServerDecorator = (
context: DecoratorContext,
target: Namespace | Interface,
options?: McpServerOptions,
) => void;

export type TypeSpecMCPDecorators = {
tool: ToolDecorator;
readonly: ReadonlyDecorator;
nondestructive: NondestructiveDecorator;
idempotent: IdempotentDecorator;
closedWorld: ClosedWorldDecorator;
resource: ResourceDecorator;
mcpServer: McpServerDecorator;
};
10 changes: 10 additions & 0 deletions packages/mcp/generated-defs/TypeSpec.MCP.ts-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// An error in the imports would mean that the decorator is not exported or
// doesn't have the right name.

import { $decorators } from "@typespec/mcp";
import type { TypeSpecMCPDecorators } from "./TypeSpec.MCP.js";

/**
* An error here would mean that the exported decorator is not using the same signature. Make sure to have export const $decName: DecNameDecorator = (...) => ...
*/
const _: TypeSpecMCPDecorators = $decorators["TypeSpec.MCP"];
114 changes: 114 additions & 0 deletions packages/mcp/lib/main.tsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import "../dist/src/tsp-index.js";

namespace TypeSpec.MCP;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
namespace TypeSpec.MCP;
namespace TypeSpec.Mcp;

do we want to keep MCP or Mcp


/**
* Declare an operation that is an MCP Tool.
* @param value - Default is true.
*/
extern dec tool(target: Reflection.Operation);

/**
* Tool does not modify its environment.
* @param value - Default is true.
*/
extern dec readonly(target: Reflection.Operation, value?: boolean);

/**
* Tool will not perform any destructive operations.
* @param value - Default is true.
*/
extern dec nondestructive(target: Reflection.Operation, value?: boolean);
/**
* Repeated calls with same args have no additional effect
* @param value - Default is true.
*/
extern dec idempotent(target: Reflection.Operation, value?: boolean);

/**
* Tool will not interacts with external entities
* @param value - Default is true.
*/
extern dec closedWorld(target: Reflection.Operation, value?: boolean);

extern dec resource(target: Reflection.Operation, uri?: valueof string);

model McpServerOptions {
name?: string;
version?: string;
instructions?: string;
}
/**
* Declare a namespace or interface as an MCP Server and provide server
* metadata.
*/
extern dec mcpServer(
target: Reflection.Namespace | Reflection.Interface,
options?: valueof McpServerOptions
);

namespace Private {
extern dec serializeAsText(target: Reflection.Model, type: unknown);
}
/**
* A long-running operation. Long-running operations may take a while
* so the server may periodically return progress notifications.
*/
model LRO<TResult> {
result: TResult;
}

@Private.serializeAsText(TDataType)
model TextResult<TDataType = never> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are all those models still used or just some original concept

type: "text";
text: string;
}

model ImageResult<MimeType extends string = string> {
type: "image";
data: FileData;
mimeType: MimeType;
}

model AudioResult<MimeType extends string = string> {
type: "audio";
data: FileData;
mimeType: MimeType;
}

model ResourceResult<TResourceType extends Resource = Resource> {
type: "resource";
resource: TResourceType;
}

model EmbeddedResource {
type: "resource";
resource: Resource;
}

model TextResource {
uri: string;
mimeType: "text/plain";
text: string;
}

model BinaryResource<MimeType extends string = string> {
uri: string;
mimeType: MimeType;
blob: FileData;
}

union Resource {
TextResource,
BinaryResource,
}

@encode(BytesKnownEncoding.base64)
scalar FileData extends bytes;

@error
model MCPError<TDataType = never> {
code: safeint;
message: string;
data?: TDataType;
}
40 changes: 40 additions & 0 deletions packages/mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@typespec/mcp",
"version": "0.1.0",
"description": "",
"exports": {
".": {
"import": "./dist/src/index.js",
"typespec": "./lib/main.tsp"
},
"./testing": {
"import": "./dist/testing/index.js"
},
"./typekit": {
"import": "./dist/src/typekit/index.js"
}
},
"imports": {
"#test/*": "./test/*"
},
"peerDependencies": {
"@typespec/compiler": "workspace:^"
},
"scripts": {
"build": "pnpm gen-extern-signature && pnpm build-tsc",
"gen-extern-signature": "tspd --enable-experimental gen-extern-signature .",
"build-tsc": "tsc -p .",
"watch-tsc": "tsc -p . --watch",
"watch": "pnpm watch-tsc",
"test": "vitest run"
},
"keywords": [],
"author": "",
"license": "MIT",
"type": "module",
"devDependencies": {
"@types/node": "~24.10.1",
"@typespec/compiler": "workspace:^",
"@typespec/tspd": "workspace:^"
}
}
91 changes: 91 additions & 0 deletions packages/mcp/src/decorators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type {
DecoratorContext,
DecoratorFunction,
Interface,
Namespace,
Operation,
Type,
} from "@typespec/compiler";
import { useStateMap, useStateSet } from "@typespec/compiler/utils";
import {
type ClosedWorldDecorator,
type IdempotentDecorator,
type McpServerDecorator,
type McpServerOptions,
type NondestructiveDecorator,
type ReadonlyDecorator,
type ResourceDecorator,
type ToolDecorator,
} from "../generated-defs/TypeSpec.MCP.js";
import { stateKeys } from "./lib.js";

function createMarkerDecorator<T extends DecoratorFunction>(
key: symbol,
validate?: (...args: Parameters<T>) => boolean,
) {
const [is, mark] = useStateSet<Parameters<T>[1]>(key);
const decorator = (...args: Parameters<T>) => {
if (validate && !validate(...args)) {
return;
}
const [context, target] = args;
mark(context.program, target);
};
return [is, mark, decorator as T] as const;
}

export const [isTool, markTool, toolDecorator] = createMarkerDecorator<ToolDecorator>(
stateKeys.tool,
);
export const [isReadonly, markReadonly, readonlyDecorator] =
createMarkerDecorator<ReadonlyDecorator>(stateKeys.readonly);
export const [isNondestructive, markNondestructive, nondestructiveDecorator] =
createMarkerDecorator<NondestructiveDecorator>(stateKeys.nondestructive);
export const [isIdempotent, markIdempotent, idempotentDecorator] =
createMarkerDecorator<IdempotentDecorator>(stateKeys.idempotent);
export const [isClosedWorld, markClosedWorld, closedWorldDecorator] =
createMarkerDecorator<ClosedWorldDecorator>(stateKeys.closedWorld);

export const [getSerializeAsText, setSerializeAsText] = useStateMap<Type, { dataType: Type }>(
stateKeys.serializeAsText,
);

export function $serializeAsText(context: DecoratorContext, target: Type, dataType: Type) {
setSerializeAsText(context.program, target, {
dataType,
});
}

export interface McpServer extends McpServerOptions {
container: Namespace | Interface;
}

export const [getMcpServer, setMcpServer] = useStateMap<Namespace | Interface, McpServer>(
stateKeys.mcpServer,
);

export const $mcpServer: McpServerDecorator = (
context: DecoratorContext,
target: Namespace | Interface,
options: McpServerOptions = {},
) => {
setMcpServer(context.program, target, {
...options,
container: target,
});
};

export interface Resource {
readonly uri?: string;
}

export const [getResource, setResource] = useStateMap<Operation, Resource>(stateKeys.resource);
export const $resource: ResourceDecorator = (
context: DecoratorContext,
target: Operation,
uri?: string,
) => {
setResource(context.program, target, {
uri,
});
};
4 changes: 4 additions & 0 deletions packages/mcp/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./decorators.js";
export { $lib } from "./lib.js";
/** @internal */
export { $decorators } from "./tsp-index.js";
Loading
Loading