From 788ffcd9c2518b69f557d96509fd66a4fb191014 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 26 Nov 2025 13:49:14 -0500 Subject: [PATCH 1/7] Move typespec mcp --- packages/mcp/LICENSE | 21 +++ packages/mcp/README.md | 24 +++ packages/mcp/generated-defs/MCP.Private.ts | 11 ++ packages/mcp/generated-defs/MCP.ts | 84 +++++++++ packages/mcp/generated-defs/MCP.ts-test.ts | 10 ++ packages/mcp/lib/main.tsp | 111 ++++++++++++ packages/mcp/package.json | 41 +++++ packages/mcp/src/decorators.ts | 79 +++++++++ packages/mcp/src/index.ts | 4 + packages/mcp/src/lib.ts | 23 +++ packages/mcp/src/tsp-index.ts | 28 +++ packages/mcp/src/typekit/index.test.ts | 90 ++++++++++ packages/mcp/src/typekit/index.ts | 197 +++++++++++++++++++++ packages/mcp/test/tester.ts | 8 + packages/mcp/testing/index.ts | 13 ++ packages/mcp/tsconfig.json | 15 ++ packages/mcp/vitest.config.ts | 4 + pnpm-lock.yaml | 50 +++++- 18 files changed, 810 insertions(+), 3 deletions(-) create mode 100644 packages/mcp/LICENSE create mode 100644 packages/mcp/README.md create mode 100644 packages/mcp/generated-defs/MCP.Private.ts create mode 100644 packages/mcp/generated-defs/MCP.ts create mode 100644 packages/mcp/generated-defs/MCP.ts-test.ts create mode 100644 packages/mcp/lib/main.tsp create mode 100644 packages/mcp/package.json create mode 100644 packages/mcp/src/decorators.ts create mode 100644 packages/mcp/src/index.ts create mode 100644 packages/mcp/src/lib.ts create mode 100644 packages/mcp/src/tsp-index.ts create mode 100644 packages/mcp/src/typekit/index.test.ts create mode 100644 packages/mcp/src/typekit/index.ts create mode 100644 packages/mcp/test/tester.ts create mode 100644 packages/mcp/testing/index.ts create mode 100644 packages/mcp/tsconfig.json create mode 100644 packages/mcp/vitest.config.ts diff --git a/packages/mcp/LICENSE b/packages/mcp/LICENSE new file mode 100644 index 00000000000..356b112bcc4 --- /dev/null +++ b/packages/mcp/LICENSE @@ -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 \ No newline at end of file diff --git a/packages/mcp/README.md b/packages/mcp/README.md new file mode 100644 index 00000000000..88c98cd1b00 --- /dev/null +++ b/packages/mcp/README.md @@ -0,0 +1,24 @@ +# @typespec/mcp + +## Overview + +This package is a TypeSpec library that provides a `tool` decorator. Support for Prompts and Resources is still in active development. + +## Troubleshooting + +See [Troubleshooting guide](https://github.com/bterlson/@typespec/mcp/blob/main/TROUBLESHOOTING.md) for help with common issues and logging. + +## Contributing + +We welcome contributions to the TypeSpec MCP Server! Whether you're fixing bugs, adding new features, or improving documentation, your contributions are welcome. + +Please read our [Contributing Guide](https://github.com/bterlson/@typespec/mcp/blob/main/CONTRIBUTING.md) for more information on how to contribute. + +## Code of Conduct + +This project has adopted the +[Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information, see the +[Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) +with any additional questions or comments. diff --git a/packages/mcp/generated-defs/MCP.Private.ts b/packages/mcp/generated-defs/MCP.Private.ts new file mode 100644 index 00000000000..765df91456d --- /dev/null +++ b/packages/mcp/generated-defs/MCP.Private.ts @@ -0,0 +1,11 @@ +import type { DecoratorContext, Model, Type } from "@typespec/compiler"; + +export type SerializeAsTextDecorator = ( + context: DecoratorContext, + target: Model, + type: Type, +) => void; + +export type MCPPrivateDecorators = { + serializeAsText: SerializeAsTextDecorator; +}; diff --git a/packages/mcp/generated-defs/MCP.ts b/packages/mcp/generated-defs/MCP.ts new file mode 100644 index 00000000000..17fdcd11ea6 --- /dev/null +++ b/packages/mcp/generated-defs/MCP.ts @@ -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 MCPDecorators = { + tool: ToolDecorator; + readonly: ReadonlyDecorator; + nondestructive: NondestructiveDecorator; + idempotent: IdempotentDecorator; + closedWorld: ClosedWorldDecorator; + resource: ResourceDecorator; + mcpServer: McpServerDecorator; +}; diff --git a/packages/mcp/generated-defs/MCP.ts-test.ts b/packages/mcp/generated-defs/MCP.ts-test.ts new file mode 100644 index 00000000000..783b8c53540 --- /dev/null +++ b/packages/mcp/generated-defs/MCP.ts-test.ts @@ -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 { MCPDecorators } from "./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 _: MCPDecorators = $decorators["MCP"]; diff --git a/packages/mcp/lib/main.tsp b/packages/mcp/lib/main.tsp new file mode 100644 index 00000000000..8622bd67994 --- /dev/null +++ b/packages/mcp/lib/main.tsp @@ -0,0 +1,111 @@ +import "../dist/src/tsp-index.js"; + +namespace 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 { + result: TResult; +} + +@Private.serializeAsText(TDataType) +model TextResult { + type: "text"; + text: string; +} + +model ImageResult { + type: "image"; + data: FileData; + mimeType: MimeType; +} + +model AudioResult { + type: "audio"; + data: FileData; + mimeType: MimeType; +} + +model ResourceResult { + type: "resource"; + resource: TResourceType; +} + +model EmbeddedResource { + type: "resource"; + resource: Resource; +} + +model TextResource { + uri: string; + mimeType: "text/plain"; + text: string; +} + +model BinaryResource { + uri: string; + mimeType: MimeType; + blob: FileData; +} + +union Resource { + TextResource, + BinaryResource, +} + +@encode(BytesKnownEncoding.base64) +scalar FileData extends bytes; + +@error +model MCPError { + code: safeint; + message: string; + data?: TDataType; +} diff --git a/packages/mcp/package.json b/packages/mcp/package.json new file mode 100644 index 00000000000..291c1dc97ff --- /dev/null +++ b/packages/mcp/package.json @@ -0,0 +1,41 @@ +{ + "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", + "format": "prettier . --write" + }, + "keywords": [], + "author": "", + "license": "MIT", + "type": "module", + "devDependencies": { + "@types/node": "~24.10.1", + "@typespec/compiler": "workspace:^", + "@typespec/tspd": "workspace:^" + } +} diff --git a/packages/mcp/src/decorators.ts b/packages/mcp/src/decorators.ts new file mode 100644 index 00000000000..b100f814f18 --- /dev/null +++ b/packages/mcp/src/decorators.ts @@ -0,0 +1,79 @@ +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/MCP.js"; +import { stateKeys } from "./lib.js"; + +function createMarkerDecorator( + key: symbol, + validate?: (...args: Parameters) => boolean, +) { + const [is, mark] = useStateSet[1]>(key); + const decorator = (...args: Parameters) => { + 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(stateKeys.tool); +export const [isReadonly, markReadonly, readonlyDecorator] = createMarkerDecorator( + stateKeys.readonly, +); +export const [isNondestructive, markNondestructive, nondestructiveDecorator] = + createMarkerDecorator(stateKeys.nondestructive); +export const [isIdempotent, markIdempotent, idempotentDecorator] = createMarkerDecorator( + stateKeys.idempotent, +); +export const [isClosedWorld, markClosedWorld, closedWorldDecorator] = createMarkerDecorator( + stateKeys.closedWorld, +); + +export const [getSerializeAsText, setSerializeAsText] = useStateMap( + 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(stateKeys.mcpServer); + +export const $mcpServer: McpServerDecorator = ( + context: DecoratorContext, + target: Namespace | Interface, + options: McpServerOptions = {}, +) => { + setMcpServer(context.program, target, { + ...options, + container: target, + }); +}; + +export interface Resource { + uri: string; +} + +export const [getResource, setResource] = useStateMap(stateKeys.resource); +export const $resource: ResourceDecorator = (context: DecoratorContext, target: Operation, uri?: string) => { + setResource(context.program, target, { + uri: uri ?? "", + }); +}; diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts new file mode 100644 index 00000000000..a39e3e9e302 --- /dev/null +++ b/packages/mcp/src/index.ts @@ -0,0 +1,4 @@ +export * from "./decorators.js"; +export { $lib } from "./lib.js"; +/** @internal */ +export { $decorators } from "./tsp-index.js"; diff --git a/packages/mcp/src/lib.ts b/packages/mcp/src/lib.ts new file mode 100644 index 00000000000..8d9570b5705 --- /dev/null +++ b/packages/mcp/src/lib.ts @@ -0,0 +1,23 @@ +import { createTypeSpecLibrary } from "@typespec/compiler"; + +export const $lib = createTypeSpecLibrary({ + name: "@typespec/mcp", + diagnostics: {}, + state: { + tool: { description: "An MCP tool" }, + resource: { description: "An MCP resource" }, + readonly: { description: "Readonly tool" }, + nondestructive: { description: "Non destructive tool" }, + idempotent: { description: "Idempotent tool" }, + closedWorld: { description: "Closed world tool" }, + serializeAsText: { + description: "Holds the type which is serialized to text", + }, + mcpServer: { + description: "Metadata about an MCP Server", + }, + }, +} as const); + +// Optional but convenient, these are meant to be used locally in your library. +export const { reportDiagnostic, createDiagnostic, stateKeys } = $lib; diff --git a/packages/mcp/src/tsp-index.ts b/packages/mcp/src/tsp-index.ts new file mode 100644 index 00000000000..499e6269d7f --- /dev/null +++ b/packages/mcp/src/tsp-index.ts @@ -0,0 +1,28 @@ +import type { MCPDecorators } from "../generated-defs/MCP.js"; +import type { MCPPrivateDecorators } from "../generated-defs/MCP.Private.js"; +import { + $mcpServer, + $resource, + $serializeAsText, + closedWorldDecorator, + idempotentDecorator, + nondestructiveDecorator, + readonlyDecorator, + toolDecorator, +} from "./decorators.js"; + +/** @internal */ +export const $decorators = { + "MCP": { + mcpServer: $mcpServer, + tool: toolDecorator, + resource: $resource, + readonly: readonlyDecorator, + nondestructive: nondestructiveDecorator, + idempotent: idempotentDecorator, + closedWorld: closedWorldDecorator, + } satisfies MCPDecorators, + "MCP.Private": { + serializeAsText: $serializeAsText, + } satisfies MCPPrivateDecorators, +}; diff --git a/packages/mcp/src/typekit/index.test.ts b/packages/mcp/src/typekit/index.test.ts new file mode 100644 index 00000000000..4443c188feb --- /dev/null +++ b/packages/mcp/src/typekit/index.test.ts @@ -0,0 +1,90 @@ +import { Tester } from "#test/tester.js"; +import { expectTypeEquals, t } from "@typespec/compiler/testing"; +import { $ } from "@typespec/compiler/typekit"; +import { describe, expect, it } from "vitest"; +import "./index.js"; + +describe("$.mcp.servers.list()", () => { + describe("discovery", () => { + it("as a top level namespace", async () => { + const { program, Service } = await Tester.compile(t.code` + @mcpServer + namespace ${t.namespace("Service")}; + `); + + const server = $(program).mcp.servers.list(); + expect(server).toHaveLength(1); + expectTypeEquals(server[0].container, Service); + }); + + it("as a top level interface", async () => { + const { program, Service } = await Tester.compile(t.code` + @mcpServer + interface ${t.interface("Service")} {} + `); + + const server = $(program).mcp.servers.list(); + expect(server).toHaveLength(1); + expectTypeEquals(server[0].container, Service); + }); + + it("as a nested namespace", async () => { + const { program, Service } = await Tester.compile(t.code` + @mcpServer + namespace Org.${t.namespace("Service")}; + `); + + const server = $(program).mcp.servers.list(); + expect(server).toHaveLength(1); + expectTypeEquals(server[0].container, Service); + }); + + it("as a nested interface", async () => { + const { program, Service } = await Tester.compile(t.code` + namespace Org; + + @mcpServer + interface ${t.interface("Service")} {} + `); + + const server = $(program).mcp.servers.list(); + expect(server).toHaveLength(1); + expectTypeEquals(server[0].container, Service); + }); + + it("collect multiple", async () => { + const { program, Service1, Service2 } = await Tester.compile(t.code` + @mcpServer + namespace ${t.namespace("Service1")} {} + namespace Group { + @mcpServer + interface ${t.interface("Service2")} {} + } + `); + + const server = $(program).mcp.servers.list(); + expect(server).toHaveLength(2); + expectTypeEquals(server[0].container, Service1); + expectTypeEquals(server[1].container, Service2); + }); + }); + + it("set properties", async () => { + const { program } = await Tester.compile(t.code` + @mcpServer(#{ + name: "MyService", + instructions: "This is a test service", + version: "0.1.0", + }) + interface Service {} + `); + + const server = $(program).mcp.servers.list(); + expect(server).toHaveLength(1); + expect(server[0]).toMatchObject({ + name: "MyService", + instructions: "This is a test service", + version: "0.1.0", + }); + }); +}); diff --git a/packages/mcp/src/typekit/index.ts b/packages/mcp/src/typekit/index.ts new file mode 100644 index 00000000000..4b341fc6a70 --- /dev/null +++ b/packages/mcp/src/typekit/index.ts @@ -0,0 +1,197 @@ +import { + ignoreDiagnostics, + type Interface, + type Model, + type Namespace, + type Operation, + type Scalar, + type Type, + type Union, +} from "@typespec/compiler"; +import { defineKit } from "@typespec/compiler/typekit"; +import { getMcpServer, isTool, type McpServer } from "../decorators.js"; +import { stateKeys } from "../lib.js"; + +export interface McpKit { + tools: { + list(server?: McpServer): Operation[]; + }; + builtins: { + get TextResult(): Model; + get LRO(): Model; + get ImageResult(): Model; + get AudioResult(): Model; + get EmbeddedResource(): Model; + get TextResource(): Model; + get BinaryResource(): Model; + get Resource(): Union; + get FileData(): Scalar; + get MCPError(): Model; + }; + textResult: { + is(type: Type): boolean; + getSerializedType(type: Model): Type | undefined; + }; + audioResult: { + is(type: Type): boolean; + }; + imageResult: { + is(type: Type): boolean; + }; + resourceResult: { + is(type: Type): boolean; + }; + servers: { + list(): McpServer[]; + }; + isKnownMcpResult(type: Type): boolean; +} +interface TypekitExtension { + /** + * Typekit for @typespec/mcp + * @experimental + */ + mcp: McpKit; +} + +declare module "@typespec/compiler/typekit" { + interface Typekit extends TypekitExtension {} +} + +function listUnder(container: Namespace | Interface) { + const ops: Operation[] = []; + + function visitNamespace(ns: Namespace) { + for (const member of ns.operations.values()) { + ops.push(member); + } + for (const member of ns.interfaces.values()) { + visitInterface(member); + } + for (const member of ns.namespaces.values()) { + visitNamespace(member); + } + } + + function visitInterface(iface: Interface) { + for (const op of iface.operations.values()) { + ops.push(op); + } + // If interfaces can contain nested interfaces or namespaces, add similar logic here + } + + if (container.kind === "Namespace") { + visitNamespace(container); + } else if (container.kind === "Interface") { + visitInterface(container); + } + + return ops; +} + +function listContainersUnder(container: Namespace): (Namespace | Interface)[] { + const result: (Namespace | Interface)[] = []; + + function visitNamespace(ns: Namespace) { + result.push(ns); + for (const member of ns.namespaces.values()) { + visitNamespace(member); + } + for (const member of ns.interfaces.values()) { + result.push(member); + } + } + + visitNamespace(container); + + return result; +} + +defineKit({ + mcp: { + tools: { + list(server?: McpServer) { + const root = server?.container ?? this.program.getGlobalNamespaceType(); + return listUnder(root).filter((x) => isTool(this.program, x)); + }, + }, + builtins: { + get BinaryResource(): Model { + return ignoreDiagnostics(this.program.resolveTypeReference("MCP.BinaryResource"))! as Model; + }, + get TextResult(): Model { + return ignoreDiagnostics(this.program.resolveTypeReference("MCP.TextResult"))! as Model; + }, + get LRO(): Model { + return ignoreDiagnostics(this.program.resolveTypeReference("MCP.LRO"))! as Model; + }, + get ImageResult(): Model { + return ignoreDiagnostics(this.program.resolveTypeReference("MCP.ImageResult"))! as Model; + }, + get AudioResult(): Model { + return ignoreDiagnostics(this.program.resolveTypeReference("MCP.AudioResult"))! as Model; + }, + get EmbeddedResource(): Model { + return ignoreDiagnostics( + this.program.resolveTypeReference("MCP.EmbeddedResource"), + )! as Model; + }, + get TextResource(): Model { + return ignoreDiagnostics(this.program.resolveTypeReference("MCP.TextResource"))! as Model; + }, + get FileData(): Scalar { + return ignoreDiagnostics(this.program.resolveTypeReference("MCP.FileData"))! as Scalar; + }, + get MCPError(): Model { + return ignoreDiagnostics(this.program.resolveTypeReference("MCP.MCPError"))! as Model; + }, + get Resource(): Union { + return ignoreDiagnostics(this.program.resolveTypeReference("MCP.Resource"))! as Union; + }, + }, + + textResult: { + is(type: Type) { + return type.kind === "Model" && type.name === "TextResult"; + }, + getSerializedType(type: Model): Type | undefined { + return this.program.stateMap(stateKeys.serializeAsText).get(type).dataType; + }, + }, + + audioResult: { + is(type: Type) { + return type.kind === "Model" && type.name === "AudioResult"; + }, + }, + + imageResult: { + is(type: Type) { + return type.kind === "Model" && type.name === "ImageResult"; + }, + }, + + resourceResult: { + is(type: Type) { + return type.kind === "Model" && type.name === "Resource"; + }, + }, + + servers: { + list() { + return listContainersUnder(this.program.getGlobalNamespaceType()) + .map((x) => getMcpServer(this.program, x)) + .filter((x) => x !== undefined); + }, + }, + + isKnownMcpResult(type) { + return ( + this.mcp.textResult.is(type) || + this.mcp.audioResult.is(type) || + this.mcp.imageResult.is(type) || + this.mcp.resourceResult.is(type) + ); + }, + }, +}); diff --git a/packages/mcp/test/tester.ts b/packages/mcp/test/tester.ts new file mode 100644 index 00000000000..c3d5f43b2d2 --- /dev/null +++ b/packages/mcp/test/tester.ts @@ -0,0 +1,8 @@ +import { resolvePath } from "@typespec/compiler"; +import { createTester } from "@typespec/compiler/testing"; + +export const Tester = createTester(resolvePath(import.meta.dirname, ".."), { + libraries: ["@typespec/mcp"], +}) + .importLibraries() + .using("MCP"); diff --git a/packages/mcp/testing/index.ts b/packages/mcp/testing/index.ts new file mode 100644 index 00000000000..a3a63566f10 --- /dev/null +++ b/packages/mcp/testing/index.ts @@ -0,0 +1,13 @@ +import { createTestLibrary } from "@typespec/compiler/testing"; +import path from "path"; +import { fileURLToPath } from "url"; + +// Get the directory name of the current module +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Use createTestLibrary helper which properly handles the types +export const TypeSpecMcp = createTestLibrary({ + name: "@typespec/mcp", + packageRoot: path.resolve(__dirname, "../.."), +}); diff --git a/packages/mcp/tsconfig.json b/packages/mcp/tsconfig.json new file mode 100644 index 00000000000..bf579f0e496 --- /dev/null +++ b/packages/mcp/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist" + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "testing/**/*.ts", + "test/**/*.ts", + "test/**/*.tsx", + "generated-defs/**/*.ts" + ], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/mcp/vitest.config.ts b/packages/mcp/vitest.config.ts new file mode 100644 index 00000000000..63cad767f57 --- /dev/null +++ b/packages/mcp/vitest.config.ts @@ -0,0 +1,4 @@ +import { defineConfig, mergeConfig } from "vitest/config"; +import { defaultTypeSpecVitestConfig } from "../../vitest.config.js"; + +export default mergeConfig(defaultTypeSpecVitestConfig, defineConfig({})); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d38e971f4e6..77105a534e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2339,6 +2339,18 @@ importers: specifier: ^4.0.4 version: 4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1) + packages/@typespec/mcp: + devDependencies: + '@types/node': + specifier: ~24.10.1 + version: 24.10.1 + '@typespec/compiler': + specifier: workspace:^ + version: link:../compiler + '@typespec/tspd': + specifier: workspace:^ + version: link:../tspd + packages/typespec-vs: devDependencies: '@typespec/internal-build-utils': @@ -4439,89 +4451,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -5554,24 +5582,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@reflink/reflink-linux-arm64-musl@0.1.19': resolution: {integrity: sha512-37iO/Dp6m5DDaC2sf3zPtx/hl9FV3Xze4xoYidrxxS9bgP3S8ALroxRK6xBG/1TtfXKTvolvp+IjrUU6ujIGmA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@reflink/reflink-linux-x64-gnu@0.1.19': resolution: {integrity: sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@reflink/reflink-linux-x64-musl@0.1.19': resolution: {integrity: sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@reflink/reflink-win32-arm64-msvc@0.1.19': resolution: {integrity: sha512-09PxnVIQcd+UOn4WAW73WU6PXL7DwGS6wPlkMhMg2zlHHG65F3vHepOw06HFCq+N42qkaNAc8AKIabWvtk6cIQ==} @@ -5664,61 +5696,73 @@ packages: resolution: {integrity: sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.49.0': resolution: {integrity: sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.49.0': resolution: {integrity: sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.49.0': resolution: {integrity: sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.49.0': resolution: {integrity: sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.49.0': resolution: {integrity: sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.49.0': resolution: {integrity: sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.49.0': resolution: {integrity: sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.49.0': resolution: {integrity: sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.40.0': resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.49.0': resolution: {integrity: sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.49.0': resolution: {integrity: sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.49.0': resolution: {integrity: sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA==} @@ -18819,7 +18863,7 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 17.0.45 + '@types/node': 24.10.1 '@types/semver@7.7.1': {} @@ -19552,7 +19596,7 @@ snapshots: algoliasearch: 4.25.3 clipanion: 4.0.0-rc.4(typanion@3.14.0) diff: 5.2.0 - ink: 3.2.0(@types/react@19.2.6)(react@17.0.2) + ink: 3.2.0(@types/react@19.2.6)(react@18.3.1) ink-text-input: 4.0.3(ink@3.2.0(@types/react@19.2.6)(react@17.0.2))(react@17.0.2) react: 17.0.2 semver: 7.7.3 @@ -19702,7 +19746,7 @@ snapshots: '@yarnpkg/plugin-git': 3.1.3(@yarnpkg/core@4.5.0(typanion@3.14.0))(typanion@3.14.0) clipanion: 4.0.0-rc.4(typanion@3.14.0) es-toolkit: 1.42.0 - ink: 3.2.0(@types/react@19.2.6)(react@18.3.1) + ink: 3.2.0(@types/react@19.2.6)(react@17.0.2) react: 17.0.2 semver: 7.7.3 tslib: 2.8.1 From 9a11abcb25aa4aedb0001abe2813c45263554cd2 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 26 Nov 2025 13:51:36 -0500 Subject: [PATCH 2/7] . --- packages/mcp/README.md | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 88c98cd1b00..56b64736bf9 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -1,24 +1,3 @@ # @typespec/mcp -## Overview - -This package is a TypeSpec library that provides a `tool` decorator. Support for Prompts and Resources is still in active development. - -## Troubleshooting - -See [Troubleshooting guide](https://github.com/bterlson/@typespec/mcp/blob/main/TROUBLESHOOTING.md) for help with common issues and logging. - -## Contributing - -We welcome contributions to the TypeSpec MCP Server! Whether you're fixing bugs, adding new features, or improving documentation, your contributions are welcome. - -Please read our [Contributing Guide](https://github.com/bterlson/@typespec/mcp/blob/main/CONTRIBUTING.md) for more information on how to contribute. - -## Code of Conduct - -This project has adopted the -[Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information, see the -[Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) -or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) -with any additional questions or comments. +TypeSpec library providing decorators for the Mode[l Context Protocol(MCP)](https://modelcontextprotocol.io/). From c44794f3d2f57a7f7f48b03c8b4a16a24a190dfb Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 26 Nov 2025 13:51:57 -0500 Subject: [PATCH 3/7] . --- pnpm-lock.yaml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77105a534e9..ba4c752462c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1196,6 +1196,18 @@ importers: specifier: ^4.0.4 version: 4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1) + packages/mcp: + devDependencies: + '@types/node': + specifier: ~24.10.1 + version: 24.10.1 + '@typespec/compiler': + specifier: workspace:^ + version: link:../compiler + '@typespec/tspd': + specifier: workspace:^ + version: link:../tspd + packages/monarch: dependencies: monaco-editor-core: @@ -2339,18 +2351,6 @@ importers: specifier: ^4.0.4 version: 4.0.12(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@4.0.12)(happy-dom@20.0.10)(jsdom@25.0.1)(tsx@4.20.6)(yaml@2.8.1) - packages/@typespec/mcp: - devDependencies: - '@types/node': - specifier: ~24.10.1 - version: 24.10.1 - '@typespec/compiler': - specifier: workspace:^ - version: link:../compiler - '@typespec/tspd': - specifier: workspace:^ - version: link:../tspd - packages/typespec-vs: devDependencies: '@typespec/internal-build-utils': From 8e062d9b6c35afa69ca09750489316c4db451ea3 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 26 Nov 2025 13:56:06 -0500 Subject: [PATCH 4/7] private --- packages/mcp/lib/main.tsp | 5 ++++- packages/mcp/src/decorators.ts | 36 ++++++++++++++++++++++------------ packages/mcp/src/tsp-index.ts | 2 +- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/packages/mcp/lib/main.tsp b/packages/mcp/lib/main.tsp index 8622bd67994..c6e28209cd7 100644 --- a/packages/mcp/lib/main.tsp +++ b/packages/mcp/lib/main.tsp @@ -42,7 +42,10 @@ model McpServerOptions { * Declare a namespace or interface as an MCP Server and provide server * metadata. */ -extern dec mcpServer(target: Reflection.Namespace | Reflection.Interface, options?: valueof McpServerOptions); +extern dec mcpServer( + target: Reflection.Namespace | Reflection.Interface, + options?: valueof McpServerOptions +); namespace Private { extern dec serializeAsText(target: Reflection.Model, type: unknown); diff --git a/packages/mcp/src/decorators.ts b/packages/mcp/src/decorators.ts index b100f814f18..a7ce895cb44 100644 --- a/packages/mcp/src/decorators.ts +++ b/packages/mcp/src/decorators.ts @@ -1,4 +1,11 @@ -import type { DecoratorContext, DecoratorFunction, Interface, Namespace, Operation, Type } from "@typespec/compiler"; +import type { + DecoratorContext, + DecoratorFunction, + Interface, + Namespace, + Operation, + Type, +} from "@typespec/compiler"; import { useStateMap, useStateSet } from "@typespec/compiler/utils"; import { type ClosedWorldDecorator, @@ -27,18 +34,17 @@ function createMarkerDecorator( return [is, mark, decorator as T] as const; } -export const [isTool, markTool, toolDecorator] = createMarkerDecorator(stateKeys.tool); -export const [isReadonly, markReadonly, readonlyDecorator] = createMarkerDecorator( - stateKeys.readonly, +export const [isTool, markTool, toolDecorator] = createMarkerDecorator( + stateKeys.tool, ); +export const [isReadonly, markReadonly, readonlyDecorator] = + createMarkerDecorator(stateKeys.readonly); export const [isNondestructive, markNondestructive, nondestructiveDecorator] = createMarkerDecorator(stateKeys.nondestructive); -export const [isIdempotent, markIdempotent, idempotentDecorator] = createMarkerDecorator( - stateKeys.idempotent, -); -export const [isClosedWorld, markClosedWorld, closedWorldDecorator] = createMarkerDecorator( - stateKeys.closedWorld, -); +export const [isIdempotent, markIdempotent, idempotentDecorator] = + createMarkerDecorator(stateKeys.idempotent); +export const [isClosedWorld, markClosedWorld, closedWorldDecorator] = + createMarkerDecorator(stateKeys.closedWorld); export const [getSerializeAsText, setSerializeAsText] = useStateMap( stateKeys.serializeAsText, @@ -54,7 +60,9 @@ export interface McpServer extends McpServerOptions { container: Namespace | Interface; } -export const [getMcpServer, setMcpServer] = useStateMap(stateKeys.mcpServer); +export const [getMcpServer, setMcpServer] = useStateMap( + stateKeys.mcpServer, +); export const $mcpServer: McpServerDecorator = ( context: DecoratorContext, @@ -72,7 +80,11 @@ export interface Resource { } export const [getResource, setResource] = useStateMap(stateKeys.resource); -export const $resource: ResourceDecorator = (context: DecoratorContext, target: Operation, uri?: string) => { +export const $resource: ResourceDecorator = ( + context: DecoratorContext, + target: Operation, + uri?: string, +) => { setResource(context.program, target, { uri: uri ?? "", }); diff --git a/packages/mcp/src/tsp-index.ts b/packages/mcp/src/tsp-index.ts index 499e6269d7f..7750c0c7f85 100644 --- a/packages/mcp/src/tsp-index.ts +++ b/packages/mcp/src/tsp-index.ts @@ -13,7 +13,7 @@ import { /** @internal */ export const $decorators = { - "MCP": { + MCP: { mcpServer: $mcpServer, tool: toolDecorator, resource: $resource, From 24d0f0a5b6c9bb82fe7ffb84823fec220833f7c6 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 26 Nov 2025 13:59:44 -0500 Subject: [PATCH 5/7] move to TypeSpec. namespace --- .../{MCP.Private.ts => TypeSpec.MCP.Private.ts} | 2 +- .../mcp/generated-defs/{MCP.ts => TypeSpec.MCP.ts} | 2 +- .../{MCP.ts-test.ts => TypeSpec.MCP.ts-test.ts} | 4 ++-- packages/mcp/lib/main.tsp | 2 +- packages/mcp/src/decorators.ts | 6 +++--- packages/mcp/src/tsp-index.ts | 12 ++++++------ 6 files changed, 14 insertions(+), 14 deletions(-) rename packages/mcp/generated-defs/{MCP.Private.ts => TypeSpec.MCP.Private.ts} (83%) rename packages/mcp/generated-defs/{MCP.ts => TypeSpec.MCP.ts} (97%) rename packages/mcp/generated-defs/{MCP.ts-test.ts => TypeSpec.MCP.ts-test.ts} (72%) diff --git a/packages/mcp/generated-defs/MCP.Private.ts b/packages/mcp/generated-defs/TypeSpec.MCP.Private.ts similarity index 83% rename from packages/mcp/generated-defs/MCP.Private.ts rename to packages/mcp/generated-defs/TypeSpec.MCP.Private.ts index 765df91456d..2eaac85a5e7 100644 --- a/packages/mcp/generated-defs/MCP.Private.ts +++ b/packages/mcp/generated-defs/TypeSpec.MCP.Private.ts @@ -6,6 +6,6 @@ export type SerializeAsTextDecorator = ( type: Type, ) => void; -export type MCPPrivateDecorators = { +export type TypeSpecMCPPrivateDecorators = { serializeAsText: SerializeAsTextDecorator; }; diff --git a/packages/mcp/generated-defs/MCP.ts b/packages/mcp/generated-defs/TypeSpec.MCP.ts similarity index 97% rename from packages/mcp/generated-defs/MCP.ts rename to packages/mcp/generated-defs/TypeSpec.MCP.ts index 17fdcd11ea6..b45822c2cff 100644 --- a/packages/mcp/generated-defs/MCP.ts +++ b/packages/mcp/generated-defs/TypeSpec.MCP.ts @@ -73,7 +73,7 @@ export type McpServerDecorator = ( options?: McpServerOptions, ) => void; -export type MCPDecorators = { +export type TypeSpecMCPDecorators = { tool: ToolDecorator; readonly: ReadonlyDecorator; nondestructive: NondestructiveDecorator; diff --git a/packages/mcp/generated-defs/MCP.ts-test.ts b/packages/mcp/generated-defs/TypeSpec.MCP.ts-test.ts similarity index 72% rename from packages/mcp/generated-defs/MCP.ts-test.ts rename to packages/mcp/generated-defs/TypeSpec.MCP.ts-test.ts index 783b8c53540..e060be8eac4 100644 --- a/packages/mcp/generated-defs/MCP.ts-test.ts +++ b/packages/mcp/generated-defs/TypeSpec.MCP.ts-test.ts @@ -2,9 +2,9 @@ // doesn't have the right name. import { $decorators } from "@typespec/mcp"; -import type { MCPDecorators } from "./MCP.js"; +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 _: MCPDecorators = $decorators["MCP"]; +const _: TypeSpecMCPDecorators = $decorators["TypeSpec.MCP"]; diff --git a/packages/mcp/lib/main.tsp b/packages/mcp/lib/main.tsp index c6e28209cd7..7a5edb44c4b 100644 --- a/packages/mcp/lib/main.tsp +++ b/packages/mcp/lib/main.tsp @@ -1,6 +1,6 @@ import "../dist/src/tsp-index.js"; -namespace MCP; +namespace TypeSpec.MCP; /** * Declare an operation that is an MCP Tool. diff --git a/packages/mcp/src/decorators.ts b/packages/mcp/src/decorators.ts index a7ce895cb44..8a59e1b2509 100644 --- a/packages/mcp/src/decorators.ts +++ b/packages/mcp/src/decorators.ts @@ -16,7 +16,7 @@ import { type ReadonlyDecorator, type ResourceDecorator, type ToolDecorator, -} from "../generated-defs/MCP.js"; +} from "../generated-defs/TypeSpec.MCP.js"; import { stateKeys } from "./lib.js"; function createMarkerDecorator( @@ -76,7 +76,7 @@ export const $mcpServer: McpServerDecorator = ( }; export interface Resource { - uri: string; + readonly uri?: string; } export const [getResource, setResource] = useStateMap(stateKeys.resource); @@ -86,6 +86,6 @@ export const $resource: ResourceDecorator = ( uri?: string, ) => { setResource(context.program, target, { - uri: uri ?? "", + uri, }); }; diff --git a/packages/mcp/src/tsp-index.ts b/packages/mcp/src/tsp-index.ts index 7750c0c7f85..50967887faa 100644 --- a/packages/mcp/src/tsp-index.ts +++ b/packages/mcp/src/tsp-index.ts @@ -1,5 +1,5 @@ -import type { MCPDecorators } from "../generated-defs/MCP.js"; -import type { MCPPrivateDecorators } from "../generated-defs/MCP.Private.js"; +import type { TypeSpecMCPDecorators } from "../generated-defs/TypeSpec.MCP.js"; +import type { TypeSpecMCPPrivateDecorators } from "../generated-defs/TypeSpec.MCP.Private.js"; import { $mcpServer, $resource, @@ -13,7 +13,7 @@ import { /** @internal */ export const $decorators = { - MCP: { + "TypeSpec.MCP": { mcpServer: $mcpServer, tool: toolDecorator, resource: $resource, @@ -21,8 +21,8 @@ export const $decorators = { nondestructive: nondestructiveDecorator, idempotent: idempotentDecorator, closedWorld: closedWorldDecorator, - } satisfies MCPDecorators, - "MCP.Private": { + } satisfies TypeSpecMCPDecorators, + "TypeSpec.MCP.Private": { serializeAsText: $serializeAsText, - } satisfies MCPPrivateDecorators, + } satisfies TypeSpecMCPPrivateDecorators, }; From bb2fe486d3571dd70cc56df47fe38923cb2a55b5 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 26 Nov 2025 11:00:00 -0800 Subject: [PATCH 6/7] Create move-typespec-mcp-2025-10-26-18-56-53.md --- .chronus/changes/move-typespec-mcp-2025-10-26-18-56-53.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .chronus/changes/move-typespec-mcp-2025-10-26-18-56-53.md diff --git a/.chronus/changes/move-typespec-mcp-2025-10-26-18-56-53.md b/.chronus/changes/move-typespec-mcp-2025-10-26-18-56-53.md new file mode 100644 index 00000000000..a91ec951a53 --- /dev/null +++ b/.chronus/changes/move-typespec-mcp-2025-10-26-18-56-53.md @@ -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` From 4ad0a8a743a85171a464a5000c555eec3a740e63 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 26 Nov 2025 14:02:49 -0500 Subject: [PATCH 7/7] . --- packages/mcp/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 291c1dc97ff..2d40a736550 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -26,8 +26,7 @@ "build-tsc": "tsc -p .", "watch-tsc": "tsc -p . --watch", "watch": "pnpm watch-tsc", - "test": "vitest run", - "format": "prettier . --write" + "test": "vitest run" }, "keywords": [], "author": "",