From 700c812cb7a8775616b128a947852a88f1ae3340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Mu=C5=A1tar?= Date: Fri, 5 Sep 2025 13:42:40 +0200 Subject: [PATCH 01/13] feat(svelte): add Svelte 5 adapter `createMcp` with API parity; export `use-mcp/svelte`; update README; keep HTTP streaming priority with SSE fallback; implement auto-retry + SSR guards --- README.md | 63 ++++- package.json | 13 +- pnpm-lock.yaml | 100 +++++++ src/svelte/createMcp.ts | 611 ++++++++++++++++++++++++++++++++++++++++ src/svelte/index.ts | 11 + 5 files changed, 792 insertions(+), 6 deletions(-) create mode 100644 src/svelte/createMcp.ts create mode 100644 src/svelte/index.ts diff --git a/README.md b/README.md index b247654..af80db1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![GitHub last commit](https://img.shields.io/github/last-commit/modelcontextprotocol/use-mcp?logo=github&style=flat&label=​)](https://github.com/modelcontextprotocol/use-mcp)  [![npm](https://img.shields.io/npm/v/use-mcp?label=​&logo=npm)](https://www.npmjs.com/package/use-mcp) ![GitHub License](https://img.shields.io/github/license/modelcontextprotocol/use-mcp) -A lightweight React hook for connecting to [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol) servers. Simplifies authentication and tool calling for AI systems implementing the MCP standard. +A lightweight client for connecting to [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol) servers. Provides a React hook and a Svelte store adapter to simplify authentication and tool calling. Try it out: [Chat Demo](https://chat.use-mcp.dev) | [MCP Inspector](https://inspector.use-mcp.dev) | [Cloudflare Workers AI Playground](https://playground.ai.cloudflare.com/) @@ -49,15 +49,15 @@ cd test && pnpm test:ui # Run tests with interactive UI - 🔄 Automatic connection management with reconnection and retries - 🔐 OAuth authentication flow handling with popup and fallback support -- 📦 Simple React hook interface for MCP integration +- 📦 Simple React hook and Svelte store adapters for MCP integration - 🧰 Full support for MCP tools, resources, and prompts - 📄 Access server resources and read their contents - 💬 Use server-provided prompt templates - 🧰 TypeScript types for editor assistance and type checking - 📝 Comprehensive logging for debugging -- 🌐 Works with both HTTP and SSE (Server-Sent Events) transports +- 🌐 Works with both HTTP and SSE (Server-Sent Events) transports (HTTP streaming recommended) -## Quick Start +## Quick Start (React) ```tsx import { useMcp } from 'use-mcp/react' @@ -146,6 +146,45 @@ function MyAIComponent() { } ``` +## Quick Start (Svelte/SvelteKit) + +```ts +// src/lib/mcp.ts +import { browser } from '$app/environment' +import { createMcp } from 'use-mcp/svelte' + +export const mcp = browser ? createMcp({ + url: 'https://your-mcp-server.com', + clientName: 'My App', + autoReconnect: true, + // transportType: 'http', // recommended; SSE is legacy +}) : undefined +``` + +```svelte + + + +{#if mcp} + {#if $mcp.state === 'failed'} +

Connection failed: {$mcp.error}

+ + + {:else if $mcp.state !== 'ready'} +

Connecting to AI service…

+ {:else} +

Available Tools: {$mcp.tools.length}

+ + {/if} +{:else} +

Loading…

+{/if} +``` + ## Setting Up OAuth Callback To handle the OAuth authentication flow, you need to set up a callback endpoint in your app. @@ -204,6 +243,20 @@ export default function OAuthCallbackPage() { } ``` +### With SvelteKit + +```svelte + + + +

Authenticating…

+

This window should close automatically.

+``` + ## API Reference ### `useMcp` Hook @@ -253,4 +306,4 @@ function useMcp(options: UseMcpOptions): UseMcpResult ## License -MIT \ No newline at end of file +MIT diff --git a/package.json b/package.json index 6a7cd6b..38aeb54 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,11 @@ "types": "./dist/react/index.d.ts", "require": "./dist/react/index.js", "import": "./dist/react/index.js" + }, + "./svelte": { + "types": "./dist/svelte/index.d.ts", + "require": "./dist/svelte/index.js", + "import": "./dist/svelte/index.js" } }, "scripts": { @@ -44,15 +49,20 @@ "husky": "^9.1.7", "prettier": "^3.5.3", "react": "^19.0.0", + "svelte": "^5.0.0", "tsup": "^8.4.0", "tsx": "^4.19.3", "typescript": "^5.8.2", "wrangler": "^4.20.2" }, + "peerDependencies": { + "svelte": "^4.2.0 || ^5.0.0" + }, "tsup": { "entry": [ "src/index.ts", - "src/react/index.ts" + "src/react/index.ts", + "src/svelte/index.ts" ], "format": [ "esm" @@ -62,6 +72,7 @@ "outDir": "dist", "external": [ "react", + "svelte", "@modelcontextprotocol/sdk" ] }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 36932dc..34e3b3e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,6 +33,9 @@ importers: react: specifier: ^19.0.0 version: 19.0.0 + svelte: + specifier: ^5.0.0 + version: 5.38.7 tsup: specifier: ^8.4.0 version: 8.4.0(tsx@4.19.3)(typescript@5.8.2) @@ -524,6 +527,9 @@ packages: resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -535,6 +541,9 @@ packages: '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} @@ -644,6 +653,11 @@ packages: cpu: [x64] os: [win32] + '@sveltejs/acorn-typescript@1.0.5': + resolution: {integrity: sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==} + peerDependencies: + acorn: ^8.9.0 + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -685,6 +699,10 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + as-table@1.0.55: resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} @@ -697,6 +715,10 @@ packages: axios@1.10.0: resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -752,6 +774,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -900,6 +926,12 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esrap@2.1.0: + resolution: {integrity: sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -1055,6 +1087,9 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1079,6 +1114,9 @@ packages: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} @@ -1088,6 +1126,9 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + magic-string@0.30.18: + resolution: {integrity: sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1415,6 +1456,10 @@ packages: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} + svelte@5.38.7: + resolution: {integrity: sha512-1ld9TPZSdUS3EtYGQzisU2nhwXoIzNQcZ71IOU9fEmltaUofQnVfW5CQuhgM/zFsZ43arZXS1BRKi0MYgUV91w==} + engines: {node: '>=18'} + tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} @@ -1579,6 +1624,9 @@ packages: youch@3.3.4: resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} + zimmerframe@1.1.2: + resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} + zod-to-json-schema@3.24.6: resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} peerDependencies: @@ -1883,12 +1931,19 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/set-array@1.2.1': {} '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -1976,6 +2031,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.35.0': optional: true + '@sveltejs/acorn-typescript@1.0.5(acorn@8.14.0)': + dependencies: + acorn: 8.14.0 + '@types/estree@1.0.6': {} '@types/react@19.0.12': @@ -2010,6 +2069,8 @@ snapshots: any-promise@1.3.0: {} + aria-query@5.3.2: {} + as-table@1.0.55: dependencies: printable-characters: 1.0.42 @@ -2028,6 +2089,8 @@ snapshots: transitivePeerDependencies: - debug + axobject-query@4.1.0: {} + balanced-match@1.0.2: {} blake3-wasm@2.1.5: {} @@ -2089,6 +2152,8 @@ snapshots: clone@1.0.4: optional: true + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2264,6 +2329,12 @@ snapshots: escape-html@1.0.3: {} + esm-env@1.2.2: {} + + esrap@2.1.0: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + etag@1.8.1: {} eventsource-parser@3.0.3: {} @@ -2433,6 +2504,10 @@ snapshots: is-promise@4.0.0: {} + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.6 + isexe@2.0.0: {} jackspeak@3.4.3: @@ -2451,12 +2526,18 @@ snapshots: load-tsconfig@0.2.5: {} + locate-character@3.0.0: {} + lodash.sortby@4.7.0: {} lodash@4.17.21: {} lru-cache@10.4.3: {} + magic-string@0.30.18: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} media-typer@1.1.0: {} @@ -2801,6 +2882,23 @@ snapshots: dependencies: has-flag: 4.0.0 + svelte@5.38.7: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.0 + '@sveltejs/acorn-typescript': 1.0.5(acorn@8.14.0) + '@types/estree': 1.0.6 + acorn: 8.14.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + clsx: 2.1.1 + esm-env: 1.2.2 + esrap: 2.1.0 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.18 + zimmerframe: 1.1.2 + tar@7.4.3: dependencies: '@isaacs/fs-minipass': 4.0.1 @@ -2981,6 +3079,8 @@ snapshots: mustache: 4.2.0 stacktracey: 2.1.8 + zimmerframe@1.1.2: {} + zod-to-json-schema@3.24.6(zod@3.25.67): dependencies: zod: 3.25.67 diff --git a/src/svelte/createMcp.ts b/src/svelte/createMcp.ts new file mode 100644 index 0000000..f0da836 --- /dev/null +++ b/src/svelte/createMcp.ts @@ -0,0 +1,611 @@ +import { readable, type Readable } from 'svelte/store' +import { + CallToolResultSchema, + JSONRPCMessage, + ListToolsResultSchema, + ListResourcesResultSchema, + ReadResourceResultSchema, + ListPromptsResultSchema, + GetPromptResultSchema, + type Resource, + type ResourceTemplate, + type Prompt, +} from '@modelcontextprotocol/sdk/types.js' +import { SSEClientTransport, type SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js' +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { auth, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js' +import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js' +import { sanitizeUrl } from 'strict-url-sanitise' + +import { BrowserOAuthClientProvider } from '../auth/browser-provider.js' +import { assert } from '../utils/assert.js' +import type { UseMcpOptions, UseMcpResult } from '../react/types.js' + +const DEFAULT_RECONNECT_DELAY = 3000 +const DEFAULT_RETRY_DELAY = 5000 +const AUTH_TIMEOUT = 5 * 60 * 1000 +type TransportType = 'http' | 'sse' + +/** + * Factory returning a Svelte store whose value mirrors UseMcpResult. + * Methods are exposed on the returned object alongside the `subscribe` method. + */ +export function createMcp(options: UseMcpOptions): Readable & Omit & { + // Explicit methods to help inference when consuming + callTool: UseMcpResult['callTool'] + listResources: UseMcpResult['listResources'] + readResource: UseMcpResult['readResource'] + listPrompts: UseMcpResult['listPrompts'] + getPrompt: UseMcpResult['getPrompt'] + retry: UseMcpResult['retry'] + disconnect: UseMcpResult['disconnect'] + authenticate: UseMcpResult['authenticate'] + clearStorage: UseMcpResult['clearStorage'] +} { + const { + url, + clientName, + clientUri, + callbackUrl = typeof window !== 'undefined' + ? sanitizeUrl(new URL('/oauth/callback', window.location.origin).toString()) + : '/oauth/callback', + storageKeyPrefix = 'mcp:auth', + clientConfig = {}, + customHeaders = {}, + debug = false, + autoRetry = false, + autoReconnect = DEFAULT_RECONNECT_DELAY, + transportType = 'auto', + preventAutoAuth = false, + onPopupWindow, + } = options + + // --- Internal mutable refs (not reactive) --- + let client: Client | null = null + let transport: Transport | null = null + let authProvider: BrowserOAuthClientProvider | null = null + let connecting = false + let isMounted = false + let connectAttempt = 0 + let authTimeout: ReturnType | null = null + let autoRetryTimer: ReturnType | null = null + let autoReconnectRef: boolean | number = autoReconnect + let stateRef: UseMcpResult['state'] = 'discovering' + let successfulTransportRef: TransportType | null = null + + // --- Helper: build initial value --- + const initialValue: UseMcpResult = { + state: 'discovering', + tools: [], + resources: [], + resourceTemplates: [], + prompts: [], + error: undefined, + authUrl: undefined, + log: [], + // Placeholder methods; replaced after store creation to capture `set` + callTool: async () => { throw new Error('MCP client not ready') }, + listResources: async () => { throw new Error('MCP client not ready') }, + readResource: async () => { throw new Error('MCP client not ready') }, + listPrompts: async () => { throw new Error('MCP client not ready') }, + getPrompt: async () => { throw new Error('MCP client not ready') }, + retry: () => {}, + disconnect: () => {}, + authenticate: () => {}, + clearStorage: () => {}, + } + + // Local mutable snapshot of the value we publish to subscribers + let current: UseMcpResult = { ...initialValue } + + // --- Logging --- + function addLog(level: UseMcpResult['log'][number]['level'], message: string, ...args: unknown[]) { + if (level === 'debug' && !debug) return + const fullMessage = args.length > 0 ? `${message} ${args.map((arg) => safeStringify(arg)).join(' ')}` : message + try { console[level](`[useMcp] ${fullMessage}`) } catch {} + if (!isMounted) return + const log = [...current.log.slice(-100), { level, message: fullMessage, timestamp: Date.now() }] + setValue({ log }) + } + + function safeStringify(v: unknown): string { + try { return JSON.stringify(v) } catch { return String(v) } + } + + function setValue(patch: Partial) { + current = { ...current, ...patch } + set(current) + } + + function setState(state: UseMcpResult['state']) { + stateRef = state + setValue({ state }) + scheduleAutoRetryIfNeeded() + } + + function clearAuthTimeout() { + if (authTimeout) { clearTimeout(authTimeout); authTimeout = null } + } + + async function doDisconnect(quiet = false) { + if (!quiet) addLog('info', 'Disconnecting...') + connecting = false + clearAuthTimeout() + const t = transport + client = null + transport = null + if (isMounted && !quiet) { + setValue({ + state: 'discovering', + tools: [], resources: [], resourceTemplates: [], prompts: [], + error: undefined, authUrl: undefined, + }) + } + if (t) { + try { await t.close(); if (!quiet) addLog('debug', 'Transport closed') } catch (e) { if (!quiet) addLog('warn', 'Error closing transport:', e as Error) } + } + } + + function scheduleAutoRetryIfNeeded() { + if (autoRetryTimer) { clearTimeout(autoRetryTimer); autoRetryTimer = null } + if (stateRef === 'failed' && autoRetry && connectAttempt > 0) { + const delay = typeof autoRetry === 'number' ? autoRetry : DEFAULT_RETRY_DELAY + addLog('info', `Connection failed, auto-retrying in ${delay}ms...`) + autoRetryTimer = setTimeout(() => { + if (isMounted && stateRef === 'failed') { + retry() + } + }, delay) + } + } + + function failConnection(errorMessage: string, connectionError?: Error) { + addLog('error', errorMessage, connectionError ?? '') + if (isMounted) { + setValue({ state: 'failed', error: errorMessage }) + const manualUrl = authProvider?.getLastAttemptedAuthUrl() || undefined + if (manualUrl) { + setValue({ authUrl: manualUrl }) + addLog('info', 'Manual authentication URL may be available.', manualUrl) + } + } + connecting = false + } + + async function connect() { + if (connecting) { addLog('debug', 'Connection attempt already in progress.'); return } + if (!isBrowser()) { addLog('debug', 'Not in browser; skipping connect.'); return } + if (!isMounted) { addLog('debug', 'Connect called before mount; aborting.'); return } + + connecting = true + connectAttempt += 1 + successfulTransportRef = null + setValue({ error: undefined, authUrl: undefined }) + setState('discovering') + addLog('info', `Connecting attempt #${connectAttempt} to ${url}...`) + + // init provider/client + if (!authProvider) { + authProvider = new BrowserOAuthClientProvider(url, { + storageKeyPrefix, + clientName, + clientUri, + callbackUrl, + preventAutoAuth, + onPopupWindow, + }) + addLog('debug', 'BrowserOAuthClientProvider initialized in connect.') + } + if (!client) { + client = new Client({ name: clientConfig.name || 'use-mcp-svelte-client', version: clientConfig.version || '0.1.0' }, { capabilities: {} }) + addLog('debug', 'MCP Client initialized in connect.') + } + + const tryConnectWithTransport = async (kind: TransportType): Promise<'success' | 'fallback' | 'auth_redirect' | 'failed'> => { + addLog('info', `Attempting connection with ${kind.toUpperCase()} transport...`) + if (stateRef !== 'authenticating') setState('connecting') + + let transportInstance: Transport + try { + assert(authProvider, 'Auth Provider must be initialized') + assert(client, 'Client must be initialized') + if (transport) { + await transport.close().catch((e: any) => addLog('warn', `Error closing previous transport: ${(e as Error)?.message || e}`)) + transport = null + } + + const commonOptions: SSEClientTransportOptions = { + authProvider, + requestInit: { headers: { Accept: 'application/json, text/event-stream', ...customHeaders } }, + } + const sanitizedUrl = sanitizeUrl(url) + const targetUrl = new URL(sanitizedUrl) + addLog('debug', `Creating ${kind.toUpperCase()} transport for URL: ${targetUrl.toString()}`) + + if (kind === 'http') { + addLog('debug', 'Creating StreamableHTTPClientTransport...') + transportInstance = new StreamableHTTPClientTransport(targetUrl, commonOptions) + } else { + addLog('debug', 'Creating SSEClientTransport...') + transportInstance = new SSEClientTransport(targetUrl, commonOptions) + } + transport = transportInstance + } catch (err) { + failConnection(`Failed to create ${kind.toUpperCase()} transport: ${err instanceof Error ? err.message : String(err)}`, + err instanceof Error ? err : undefined) + return 'failed' + } + + transportInstance.onmessage = (message: JSONRPCMessage) => { + addLog('debug', `[Transport] Received: ${safeStringify(message)}`) + // @ts-ignore + client?.handleMessage?.(message) + } + transportInstance.onerror = (err: Error) => { + addLog('warn', `Transport error event (${kind.toUpperCase()}):`, err) + failConnection(`Transport error (${kind.toUpperCase()}): ${err.message}`, err) + } + transportInstance.onclose = () => { + if (!isMounted || connecting) return + addLog('info', `Transport connection closed (${successfulTransportRef || 'unknown'} type).`) + const currentState = stateRef + const ar = autoReconnectRef + if (currentState === 'ready' && ar) { + const delay = typeof ar === 'number' ? ar : DEFAULT_RECONNECT_DELAY + addLog('info', `Attempting to reconnect in ${delay}ms...`) + setState('connecting') + setTimeout(() => { if (isMounted) connect() }, delay) + } else if (currentState !== 'failed' && currentState !== 'authenticating') { + failConnection('Connection closed unexpectedly.') + } + } + + try { + addLog('info', `Connecting client via ${kind.toUpperCase()}...`) + await client!.connect(transportInstance) + addLog('info', `Client connected via ${kind.toUpperCase()}. Loading tools, resources, and prompts...`) + successfulTransportRef = kind + setState('loading') + + const toolsResponse = await client!.request({ method: 'tools/list' }, ListToolsResultSchema) + let resourcesResponse: { resources: Resource[]; resourceTemplates?: ResourceTemplate[] } = { resources: [], resourceTemplates: [] } + try { resourcesResponse = await client!.request({ method: 'resources/list' }, ListResourcesResultSchema) } catch (e) { addLog('debug', 'Server does not support resources/list method', e as Error) } + let promptsResponse: { prompts: Prompt[] } = { prompts: [] } + try { promptsResponse = await client!.request({ method: 'prompts/list' }, ListPromptsResultSchema) } catch (e) { addLog('debug', 'Server does not support prompts/list method', e as Error) } + + if (isMounted) { + setValue({ + tools: toolsResponse.tools, + resources: resourcesResponse.resources, + resourceTemplates: Array.isArray(resourcesResponse.resourceTemplates) ? resourcesResponse.resourceTemplates : [], + prompts: promptsResponse.prompts, + state: 'ready', + }) + addLog('info', `Ready. Tools: ${toolsResponse.tools.length}, Resources: ${resourcesResponse.resources.length}, Prompts: ${promptsResponse.prompts.length}`) + } + return 'success' + } catch (err) { + const errorMessage = err instanceof Error ? err.message : String(err) + const errorInstance = err instanceof Error ? err : new Error(String(err)) + addLog('warn', `Error during connect (${kind.toUpperCase()}): ${errorMessage}`) + + if ( + errorInstance instanceof UnauthorizedError || + errorMessage.includes('Unauthorized') || errorMessage.includes('401') + ) { + if (preventAutoAuth) { + setState('pending_auth') + try { + assert(authProvider, 'Auth Provider must be initialized') + const authUrl = await authProvider.prepareAuthorizationUrl(new URL('about:blank')) + setValue({ authUrl }) + addLog('info', 'Authentication required. Manual URL prepared (preventAutoAuth=true).') + return 'failed' + } catch (prepErr) { + failConnection(`Failed to prepare authorization URL: ${prepErr instanceof Error ? prepErr.message : String(prepErr)}`, + prepErr instanceof Error ? prepErr : undefined) + return 'failed' + } + } + + setState('authenticating') + clearAuthTimeout() + authTimeout = setTimeout(() => { + if (!isMounted) return + failConnection('Authentication timed out. Please try again.') + }, AUTH_TIMEOUT) + + try { + assert(authProvider, 'Auth Provider must be initialized') + const authResult = await auth(authProvider, { serverUrl: url }) + if (!isMounted) return 'failed' + if (authResult === 'AUTHORIZED') { + addLog('info', 'Authentication successful. Re-attempting connection...') + clearAuthTimeout() + connecting = false + connect() + return 'failed' + } else if (authResult === 'REDIRECT') { + addLog('info', 'Redirecting for authentication. Waiting for callback...') + return 'auth_redirect' + } + } catch (sdkAuthError) { + if (!isMounted) return 'failed' + clearAuthTimeout() + failConnection(`Failed to initiate authentication: ${sdkAuthError instanceof Error ? sdkAuthError.message : String(sdkAuthError)}`, + sdkAuthError instanceof Error ? sdkAuthError : undefined) + return 'failed' + } + } + + failConnection(`Failed to connect via ${kind.toUpperCase()}: ${errorMessage}`, errorInstance) + return 'failed' + } + } + + let finalStatus: 'success' | 'auth_redirect' | 'failed' | 'fallback' = 'failed' + if (transportType === 'sse') { + finalStatus = await tryConnectWithTransport('sse') + } else if (transportType === 'http') { + finalStatus = await tryConnectWithTransport('http') + } else { + const httpResult = await tryConnectWithTransport('http') + if (httpResult === 'fallback' && isMounted && stateRef !== 'authenticating') { + addLog('info', 'HTTP failed, attempting SSE fallback...') + finalStatus = await tryConnectWithTransport('sse') + } else { + finalStatus = httpResult + } + } + + if (finalStatus === 'success' || finalStatus === 'failed') connecting = false + addLog('debug', `Connection sequence finished with status: ${finalStatus}`) + } + + async function callTool(name: string, args?: Record) { + if (stateRef !== 'ready' || !client) { + throw new Error(`MCP client is not ready (current state: ${current.state}). Cannot call tool "${name}".`) + } + addLog('info', `Calling tool: ${name}`, args) + try { + const result = await client.request({ method: 'tools/call', params: { name, arguments: args } }, CallToolResultSchema) + addLog('info', `Tool "${name}" call successful.`) + return result + } catch (err) { + addLog('error', `Error calling tool "${name}": ${err instanceof Error ? err.message : String(err)}`, err as Error) + const errorInstance = err instanceof Error ? err : new Error(String(err)) + if ( + errorInstance instanceof UnauthorizedError || + errorInstance.message.includes('Unauthorized') || + errorInstance.message.includes('401') + ) { + addLog('warn', 'Tool call unauthorized, attempting re-authentication...') + setState('authenticating') + clearAuthTimeout() + authTimeout = setTimeout(() => {}, AUTH_TIMEOUT) + try { + assert(authProvider, 'Auth Provider not available for tool re-auth') + const authResult = await auth(authProvider, { serverUrl: url }) + if (!isMounted) return + if (authResult === 'AUTHORIZED') { + addLog('info', 'Re-authentication successful. Reconnecting...') + clearAuthTimeout() + connecting = false + connect() + } else if (authResult === 'REDIRECT') { + addLog('info', 'Redirecting for re-authentication for tool call.') + } + } catch (sdkAuthError) { + if (!isMounted) return + clearAuthTimeout() + failConnection(`Re-authentication failed: ${sdkAuthError instanceof Error ? sdkAuthError.message : String(sdkAuthError)}`, + sdkAuthError instanceof Error ? sdkAuthError : undefined) + } + } + if (current.state !== 'authenticating') throw err as any + return undefined as any + } + } + + async function listResources() { + if (stateRef !== 'ready' || !client) { + throw new Error(`MCP client is not ready (current state: ${current.state}). Cannot list resources.`) + } + addLog('info', 'Listing resources...') + try { + const resourcesResponse = await client.request({ method: 'resources/list' }, ListResourcesResultSchema) + if (isMounted) { + setValue({ + resources: resourcesResponse.resources, + resourceTemplates: Array.isArray(resourcesResponse.resourceTemplates) ? resourcesResponse.resourceTemplates : [], + }) + addLog('info', `Listed ${resourcesResponse.resources.length} resources, ${Array.isArray(resourcesResponse.resourceTemplates) ? resourcesResponse.resourceTemplates.length : 0} resource templates.`) + } + } catch (err) { + addLog('error', `Error listing resources: ${err instanceof Error ? err.message : String(err)}`, err as Error) + throw err + } + } + + async function readResource(uri: string) { + if (stateRef !== 'ready' || !client) { + throw new Error(`MCP client is not ready (current state: ${current.state}). Cannot read resource "${uri}".`) + } + addLog('info', `Reading resource: ${uri}`) + try { + const result = await client.request({ method: 'resources/read', params: { uri } }, ReadResourceResultSchema) + addLog('info', `Resource "${uri}" read successfully`) + return result + } catch (err) { + addLog('error', `Error reading resource "${uri}": ${err instanceof Error ? err.message : String(err)}`, err as Error) + throw err + } + } + + async function listPrompts() { + if (stateRef !== 'ready' || !client) { + throw new Error(`MCP client is not ready (current state: ${current.state}). Cannot list prompts.`) + } + addLog('info', 'Listing prompts...') + try { + const promptsResponse = await client.request({ method: 'prompts/list' }, ListPromptsResultSchema) + if (isMounted) { + setValue({ prompts: promptsResponse.prompts }) + addLog('info', `Listed ${promptsResponse.prompts.length} prompts.`) + } + } catch (err) { + addLog('error', `Error listing prompts: ${err instanceof Error ? err.message : String(err)}`, err as Error) + throw err + } + } + + async function getPrompt(name: string, args?: Record) { + if (stateRef !== 'ready' || !client) { + throw new Error(`MCP client is not ready (current state: ${current.state}). Cannot get prompt "${name}".`) + } + addLog('info', `Getting prompt: ${name}`, args) + try { + const result = await client.request({ method: 'prompts/get', params: { name, arguments: args } }, GetPromptResultSchema) + addLog('info', `Prompt "${name}" retrieved successfully`) + return result + } catch (err) { + addLog('error', `Error getting prompt "${name}": ${err instanceof Error ? err.message : String(err)}`, err as Error) + throw err + } + } + + function retry() { + if (stateRef === 'failed') { + addLog('info', 'Retry requested...') + connect() + } else { + addLog('warn', `Retry called but state is not 'failed' (state: ${stateRef}). Ignoring.`) + } + } + + async function authenticate() { + addLog('info', 'Manual authentication requested...') + const s = stateRef + if (s === 'failed') { + addLog('info', 'Attempting to reconnect and authenticate via retry...') + retry() + } else if (s === 'pending_auth') { + setState('authenticating') + clearAuthTimeout() + authTimeout = setTimeout(() => {}, AUTH_TIMEOUT) + try { + assert(authProvider, 'Auth Provider not available for manual auth') + const authResult = await auth(authProvider, { serverUrl: url }) + if (!isMounted) return + if (authResult === 'AUTHORIZED') { + addLog('info', 'Manual authentication successful. Re-attempting connection...') + clearAuthTimeout() + connecting = false + connect() + } else if (authResult === 'REDIRECT') { + addLog('info', 'Redirecting for manual authentication. Waiting for callback...') + } + } catch (authError) { + if (!isMounted) return + clearAuthTimeout() + failConnection(`Manual authentication failed: ${authError instanceof Error ? authError.message : String(authError)}`, + authError instanceof Error ? authError : undefined) + } + } else if (s === 'authenticating') { + addLog('warn', 'Already attempting authentication.') + } else if (s === 'ready') { + addLog('info', 'Already authenticated and connected.') + } else { + addLog('warn', `Authentication requested in state ${s}. Will attempt connect -> auth.`) + connect() + } + } + + function clearStorage() { + if (authProvider) { + const count = authProvider.clearStorage() + addLog('info', `Cleared ${count} item(s) from localStorage for ${url}.`) + setValue({ authUrl: undefined }) + doDisconnect() + } else { + addLog('warn', 'Auth provider not initialized, cannot clear storage.') + } + } + + // Store start/stop lifecycle — start when there is at least one subscriber + let set: (v: UseMcpResult) => void + const store = readable(initialValue, (start) => { + set = start + isMounted = true + autoReconnectRef = autoReconnect + addLog('debug', 'createMcp mounted, initiating connection.') + connectAttempt = 0 + + // Initialize/refresh provider on mount/options change + if (isBrowser()) { + if (!authProvider || authProvider.serverUrl !== url) { + authProvider = new BrowserOAuthClientProvider(url, { + storageKeyPrefix, + clientName, + clientUri, + callbackUrl, + preventAutoAuth, + onPopupWindow, + }) + addLog('debug', 'BrowserOAuthClientProvider initialized/updated on mount.') + } + // Auth callback listener + const messageHandler = (event: MessageEvent) => { + if (event.origin !== window.location.origin) return + if (event.data?.type === 'mcp_auth_callback') { + addLog('info', 'Received auth callback message.', event.data) + clearAuthTimeout() + if (event.data.success) { + addLog('info', 'Authentication successful via popup. Reconnecting client...') + connecting = false + connect() + } else { + failConnection(`Authentication failed in callback: ${event.data.error || 'Unknown reason.'}`) + } + } + } + window.addEventListener('message', messageHandler) + + // Kick off connection + connect() + + return () => { + window.removeEventListener('message', messageHandler) + isMounted = false + clearAuthTimeout() + if (autoRetryTimer) { clearTimeout(autoRetryTimer); autoRetryTimer = null } + void doDisconnect(true) + } + } + + // SSR: no lifecycle, just noop stop + return () => { + isMounted = false + } + }) as Readable & any + + // Attach methods to the store object + Object.assign(store, { + callTool, + listResources, + readResource, + listPrompts, + getPrompt, + retry, + disconnect: () => { void doDisconnect() }, + authenticate: () => { void authenticate() }, + clearStorage, + }) + + return store +} + +function isBrowser() { + return typeof window !== 'undefined' && typeof document !== 'undefined' +} diff --git a/src/svelte/index.ts b/src/svelte/index.ts new file mode 100644 index 0000000..b1872d5 --- /dev/null +++ b/src/svelte/index.ts @@ -0,0 +1,11 @@ +/** + * Entry point for the use-mcp Svelte integration. + * Exposes a Svelte store factory mirroring the React hook contract. + */ + +export { createMcp } from './createMcp.js' +export type { UseMcpOptions, UseMcpResult } from '../react/types.js' + +// Re-export core types for convenience +export type { Tool, Resource, ResourceTemplate, Prompt } from '@modelcontextprotocol/sdk/types.js' + From 44329a48a36c6158cf263087be08fa101d6b2914 Mon Sep 17 00:00:00 2001 From: gary149 Date: Fri, 5 Sep 2025 18:17:29 +0200 Subject: [PATCH 02/13] =?UTF-8?q?fix(svelte):=20enable=20HTTP=E2=86=92SSE?= =?UTF-8?q?=20fallback;=20accept=20alt=20auth=20callback=20event;=20enforc?= =?UTF-8?q?e=20auth=20timeouts;=20split=20transport=20headers;=20reuse=20s?= =?UTF-8?q?anitized=20serverUrl\n\n-=20Auto=20fallback=20from=20HTTP=20to?= =?UTF-8?q?=20SSE=20in=20transportType=20'auto'\n-=20Listen=20for=20both?= =?UTF-8?q?=20'mcp=5Fauth=5Fcallback'=20and=20'mcp:oauthCallback'\n-=20Rep?= =?UTF-8?q?lace=20no-op=20auth=20timeouts=20with=20failing=20timeout=20han?= =?UTF-8?q?dler\n-=20Use=20SSE-specific=20Accept=20header;=20avoid=20SSE?= =?UTF-8?q?=20Accept=20for=20HTTP\n-=20Sanitize=20and=20reuse=20serverUrl?= =?UTF-8?q?=20consistently=20for=20auth=20+=20transports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/svelte/createMcp.ts | 56 ++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/src/svelte/createMcp.ts b/src/svelte/createMcp.ts index f0da836..0340857 100644 --- a/src/svelte/createMcp.ts +++ b/src/svelte/createMcp.ts @@ -11,7 +11,7 @@ import { type ResourceTemplate, type Prompt, } from '@modelcontextprotocol/sdk/types.js' -import { SSEClientTransport, type SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js' +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { auth, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js' @@ -62,6 +62,7 @@ export function createMcp(options: UseMcpOptions): Readable & Omit } = options // --- Internal mutable refs (not reactive) --- + const serverUrl = new URL(sanitizeUrl(url)).toString() let client: Client | null = null let transport: Transport | null = null let authProvider: BrowserOAuthClientProvider | null = null @@ -183,11 +184,11 @@ export function createMcp(options: UseMcpOptions): Readable & Omit successfulTransportRef = null setValue({ error: undefined, authUrl: undefined }) setState('discovering') - addLog('info', `Connecting attempt #${connectAttempt} to ${url}...`) + addLog('info', `Connecting attempt #${connectAttempt} to ${serverUrl}...`) // init provider/client if (!authProvider) { - authProvider = new BrowserOAuthClientProvider(url, { + authProvider = new BrowserOAuthClientProvider(serverUrl, { storageKeyPrefix, clientName, clientUri, @@ -215,20 +216,22 @@ export function createMcp(options: UseMcpOptions): Readable & Omit transport = null } - const commonOptions: SSEClientTransportOptions = { - authProvider, - requestInit: { headers: { Accept: 'application/json, text/event-stream', ...customHeaders } }, - } - const sanitizedUrl = sanitizeUrl(url) - const targetUrl = new URL(sanitizedUrl) + const baseOpts = { authProvider } as const + const targetUrl = new URL(serverUrl) addLog('debug', `Creating ${kind.toUpperCase()} transport for URL: ${targetUrl.toString()}`) if (kind === 'http') { addLog('debug', 'Creating StreamableHTTPClientTransport...') - transportInstance = new StreamableHTTPClientTransport(targetUrl, commonOptions) + transportInstance = new StreamableHTTPClientTransport(targetUrl, { + ...baseOpts, + requestInit: { headers: { ...customHeaders } }, + }) } else { addLog('debug', 'Creating SSEClientTransport...') - transportInstance = new SSEClientTransport(targetUrl, commonOptions) + transportInstance = new SSEClientTransport(targetUrl, { + ...baseOpts, + requestInit: { headers: { Accept: 'text/event-stream', ...customHeaders } }, + }) } transport = transportInstance } catch (err) { @@ -318,7 +321,7 @@ export function createMcp(options: UseMcpOptions): Readable & Omit try { assert(authProvider, 'Auth Provider must be initialized') - const authResult = await auth(authProvider, { serverUrl: url }) + const authResult = await auth(authProvider, { serverUrl }) if (!isMounted) return 'failed' if (authResult === 'AUTHORIZED') { addLog('info', 'Authentication successful. Re-attempting connection...') @@ -344,15 +347,15 @@ export function createMcp(options: UseMcpOptions): Readable & Omit } } - let finalStatus: 'success' | 'auth_redirect' | 'failed' | 'fallback' = 'failed' + let finalStatus: 'success' | 'auth_redirect' | 'failed' = 'failed' if (transportType === 'sse') { finalStatus = await tryConnectWithTransport('sse') } else if (transportType === 'http') { finalStatus = await tryConnectWithTransport('http') } else { const httpResult = await tryConnectWithTransport('http') - if (httpResult === 'fallback' && isMounted && stateRef !== 'authenticating') { - addLog('info', 'HTTP failed, attempting SSE fallback...') + if (httpResult !== 'success' && httpResult !== 'auth_redirect') { + addLog('info', 'HTTP connect failed; attempting SSE fallback...') finalStatus = await tryConnectWithTransport('sse') } else { finalStatus = httpResult @@ -383,10 +386,13 @@ export function createMcp(options: UseMcpOptions): Readable & Omit addLog('warn', 'Tool call unauthorized, attempting re-authentication...') setState('authenticating') clearAuthTimeout() - authTimeout = setTimeout(() => {}, AUTH_TIMEOUT) + authTimeout = setTimeout(() => { + if (!isMounted) return + failConnection('Authentication timed out. Please try again.') + }, AUTH_TIMEOUT) try { assert(authProvider, 'Auth Provider not available for tool re-auth') - const authResult = await auth(authProvider, { serverUrl: url }) + const authResult = await auth(authProvider, { serverUrl }) if (!isMounted) return if (authResult === 'AUTHORIZED') { addLog('info', 'Re-authentication successful. Reconnecting...') @@ -493,10 +499,13 @@ export function createMcp(options: UseMcpOptions): Readable & Omit } else if (s === 'pending_auth') { setState('authenticating') clearAuthTimeout() - authTimeout = setTimeout(() => {}, AUTH_TIMEOUT) + authTimeout = setTimeout(() => { + if (!isMounted) return + failConnection('Authentication timed out. Please try again.') + }, AUTH_TIMEOUT) try { assert(authProvider, 'Auth Provider not available for manual auth') - const authResult = await auth(authProvider, { serverUrl: url }) + const authResult = await auth(authProvider, { serverUrl }) if (!isMounted) return if (authResult === 'AUTHORIZED') { addLog('info', 'Manual authentication successful. Re-attempting connection...') @@ -525,7 +534,7 @@ export function createMcp(options: UseMcpOptions): Readable & Omit function clearStorage() { if (authProvider) { const count = authProvider.clearStorage() - addLog('info', `Cleared ${count} item(s) from localStorage for ${url}.`) + addLog('info', `Cleared ${count} item(s) from localStorage for ${serverUrl}.`) setValue({ authUrl: undefined }) doDisconnect() } else { @@ -544,8 +553,8 @@ export function createMcp(options: UseMcpOptions): Readable & Omit // Initialize/refresh provider on mount/options change if (isBrowser()) { - if (!authProvider || authProvider.serverUrl !== url) { - authProvider = new BrowserOAuthClientProvider(url, { + if (!authProvider || authProvider.serverUrl !== serverUrl) { + authProvider = new BrowserOAuthClientProvider(serverUrl, { storageKeyPrefix, clientName, clientUri, @@ -558,7 +567,8 @@ export function createMcp(options: UseMcpOptions): Readable & Omit // Auth callback listener const messageHandler = (event: MessageEvent) => { if (event.origin !== window.location.origin) return - if (event.data?.type === 'mcp_auth_callback') { + const t = (event as any)?.data?.type + if (t === 'mcp_auth_callback' || t === 'mcp:oauthCallback') { addLog('info', 'Received auth callback message.', event.data) clearAuthTimeout() if (event.data.success) { From 4c8213d95a5069daf4ba6b6d845121cab279015f Mon Sep 17 00:00:00 2001 From: gary149 Date: Fri, 5 Sep 2025 18:24:24 +0200 Subject: [PATCH 03/13] chore(pkg): prepare for npm publish under @gary149/use-mcp\n\n- Add svelte field, sideEffects=false, publishConfig.access=public\n- Set repository/homepage/bugs to fork\n- Add prepublishOnly build script\n- Bump version to 0.1.0 and set scope name\n\nfeat(svelte): DX improvements\n- Re-export onMcpAuthorization from /svelte entry\n- Accept cross-origin auth via allowedOrigins option\n- Make .callTool available by attaching methods to snapshot\n\ndocs: update README examples to @gary149/use-mcp and add CORS note --- README.md | 14 ++++++++------ package.json | 17 ++++++++++++++--- src/react/types.ts | 6 ++++++ src/svelte/createMcp.ts | 16 +++++++++++++++- src/svelte/index.ts | 2 +- 5 files changed, 44 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index af80db1..180bfb8 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,11 @@ Try it out: [Chat Demo](https://chat.use-mcp.dev) | [MCP Inspector](https://insp ## Installation ```bash -npm install use-mcp +npm install @gary149/use-mcp # or -pnpm add use-mcp +pnpm add @gary149/use-mcp # or -yarn add use-mcp +yarn add @gary149/use-mcp ``` ## Development @@ -60,7 +60,7 @@ cd test && pnpm test:ui # Run tests with interactive UI ## Quick Start (React) ```tsx -import { useMcp } from 'use-mcp/react' +import { useMcp } from '@gary149/use-mcp/react' function MyAIComponent() { const { @@ -151,7 +151,7 @@ function MyAIComponent() { ```ts // src/lib/mcp.ts import { browser } from '$app/environment' -import { createMcp } from 'use-mcp/svelte' +import { createMcp } from '@gary149/use-mcp/svelte' export const mcp = browser ? createMcp({ url: 'https://your-mcp-server.com', @@ -249,7 +249,7 @@ export default function OAuthCallbackPage() { @@ -257,6 +257,8 @@ export default function OAuthCallbackPage() {

This window should close automatically.

``` +Note: When using HTTP streaming transport across origins, ensure your MCP server CORS configuration allows and exposes the `Mcp-Session-Id` header so the browser can maintain the session. If your OAuth callback is hosted on a different origin (e.g., auth subdomain), pass `allowedOrigins: ['https://auth.example.com']` to `createMcp(...)` so the popup callback message is accepted. + ## API Reference ### `useMcp` Hook diff --git a/package.json b/package.json index 38aeb54..8f2d8e7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { - "name": "use-mcp", - "repository": "https://github.com/modelcontextprotocol/use-mcp", - "version": "0.0.21", + "name": "@gary149/use-mcp", + "repository": "https://github.com/gary149/use-mcp", + "version": "0.1.0", + "license": "MIT", "type": "module", "files": [ "dist", @@ -25,12 +26,19 @@ "import": "./dist/svelte/index.js" } }, + "svelte": "./dist/svelte/index.js", + "sideEffects": false, + "homepage": "https://github.com/gary149/use-mcp#readme", + "bugs": { + "url": "https://github.com/gary149/use-mcp/issues" + }, "scripts": { "install:all": "concurrently 'pnpm install' 'cd examples/chat-ui && pnpm install' 'cd examples/inspector && pnpm install' 'cd examples/servers/hono-mcp && pnpm install' 'cd examples/servers/cf-agents && pnpm install'", "dev": "concurrently 'pnpm:build:watch' 'cd examples/chat-ui && pnpm dev' 'cd examples/inspector && pnpm dev' 'sleep 1 && cd examples/servers/hono-mcp && pnpm dev' 'sleep 2 && cd examples/servers/cf-agents && pnpm dev'", "deploy:all": "concurrently 'pnpm build:site && pnpm deploy:site' 'cd examples/chat-ui && pnpm run deploy' 'cd examples/inspector && pnpm run deploy'", "build:watch": "tsup --watch", "build": "tsup", + "prepublishOnly": "pnpm build", "check": "prettier --check . && tsc", "prettier:fix": "prettier --write .", "fix:oranda": "sed -i 's/```tsx/```ts/g' README.md", @@ -38,6 +46,9 @@ "deploy:site": "npx wrangler deploy", "prepare": "husky" }, + "publishConfig": { + "access": "public" + }, "dependencies": { "@modelcontextprotocol/sdk": "^1.13.3", "strict-url-sanitise": "^0.0.1" diff --git a/src/react/types.ts b/src/react/types.ts index b80df54..e094411 100644 --- a/src/react/types.ts +++ b/src/react/types.ts @@ -30,6 +30,12 @@ export type UseMcpOptions = { transportType?: 'auto' | 'http' | 'sse' /** Prevent automatic authentication popup on initial connection (default: false) */ preventAutoAuth?: boolean + /** + * Allow receiving OAuth callback messages from additional origins. + * By default only the same-origin as the app is accepted. Add entries + * like 'https://auth.example.com' if your login page is hosted elsewhere. + */ + allowedOrigins?: string[] /** * Callback function that is invoked just before the authentication popup window is opened. * @param url The URL that will be opened in the popup. diff --git a/src/svelte/createMcp.ts b/src/svelte/createMcp.ts index 0340857..b3182a1 100644 --- a/src/svelte/createMcp.ts +++ b/src/svelte/createMcp.ts @@ -59,6 +59,7 @@ export function createMcp(options: UseMcpOptions): Readable & Omit transportType = 'auto', preventAutoAuth = false, onPopupWindow, + allowedOrigins = [], } = options // --- Internal mutable refs (not reactive) --- @@ -566,7 +567,7 @@ export function createMcp(options: UseMcpOptions): Readable & Omit } // Auth callback listener const messageHandler = (event: MessageEvent) => { - if (event.origin !== window.location.origin) return + if (event.origin !== window.location.origin && !allowedOrigins.includes(event.origin)) return const t = (event as any)?.data?.type if (t === 'mcp_auth_callback' || t === 'mcp:oauthCallback') { addLog('info', 'Received auth callback message.', event.data) @@ -613,6 +614,19 @@ export function createMcp(options: UseMcpOptions): Readable & Omit clearStorage, }) + // Also attach methods to the current snapshot so $mcp.callTool works + Object.assign(current, { + callTool, + listResources, + readResource, + listPrompts, + getPrompt, + retry, + disconnect: () => { void doDisconnect() }, + authenticate: () => { void authenticate() }, + clearStorage, + }) + return store } diff --git a/src/svelte/index.ts b/src/svelte/index.ts index b1872d5..55f5e2c 100644 --- a/src/svelte/index.ts +++ b/src/svelte/index.ts @@ -5,7 +5,7 @@ export { createMcp } from './createMcp.js' export type { UseMcpOptions, UseMcpResult } from '../react/types.js' +export { onMcpAuthorization } from '../auth/callback.js' // Re-export core types for convenience export type { Tool, Resource, ResourceTemplate, Prompt } from '@modelcontextprotocol/sdk/types.js' - From 26f169db9c32363109b0c7e66039da173e84f1a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Mu=C5=A1tar?= Date: Fri, 5 Sep 2025 19:26:16 +0200 Subject: [PATCH 04/13] feat(myapp): re-add SvelteKit test app using @gary149/use-mcp@0.1.1 with tools list and call UI; add OAuth callback route --- myapp/package.json | 27 +++++++++++ myapp/src/app.d.ts | 12 +++++ myapp/src/app.html | 13 ++++++ myapp/src/lib/mcp.ts | 12 +++++ myapp/src/routes/+layout.svelte | 6 +++ myapp/src/routes/+page.svelte | 49 ++++++++++++++++++++ myapp/src/routes/oauth/callback/+page.svelte | 9 ++++ myapp/svelte.config.js | 13 ++++++ myapp/tsconfig.json | 9 ++++ myapp/vite.config.ts | 7 +++ 10 files changed, 157 insertions(+) create mode 100644 myapp/package.json create mode 100644 myapp/src/app.d.ts create mode 100644 myapp/src/app.html create mode 100644 myapp/src/lib/mcp.ts create mode 100644 myapp/src/routes/+layout.svelte create mode 100644 myapp/src/routes/+page.svelte create mode 100644 myapp/src/routes/oauth/callback/+page.svelte create mode 100644 myapp/svelte.config.js create mode 100644 myapp/tsconfig.json create mode 100644 myapp/vite.config.ts diff --git a/myapp/package.json b/myapp/package.json new file mode 100644 index 0000000..020df22 --- /dev/null +++ b/myapp/package.json @@ -0,0 +1,27 @@ +{ + "name": "myapp", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^6.0.0", + "@sveltejs/kit": "^2.22.0", + "@sveltejs/vite-plugin-svelte": "^6.0.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "typescript": "^5.8.2", + "vite": "^7.1.4" + }, + "dependencies": { + "@gary149/use-mcp": "^0.1.1" + } +} + diff --git a/myapp/src/app.d.ts b/myapp/src/app.d.ts new file mode 100644 index 0000000..8cb1955 --- /dev/null +++ b/myapp/src/app.d.ts @@ -0,0 +1,12 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} +export {}; + diff --git a/myapp/src/app.html b/myapp/src/app.html new file mode 100644 index 0000000..c9835be --- /dev/null +++ b/myapp/src/app.html @@ -0,0 +1,13 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + + + diff --git a/myapp/src/lib/mcp.ts b/myapp/src/lib/mcp.ts new file mode 100644 index 0000000..cf0c5f3 --- /dev/null +++ b/myapp/src/lib/mcp.ts @@ -0,0 +1,12 @@ +import { browser } from '$app/environment' +import { createMcp } from '@gary149/use-mcp/svelte' + +export const mcp = browser + ? createMcp({ + url: 'https://huggingface.co/mcp', + clientName: 'My App', + autoReconnect: true, + transportType: 'http' + }) + : undefined + diff --git a/myapp/src/routes/+layout.svelte b/myapp/src/routes/+layout.svelte new file mode 100644 index 0000000..d875192 --- /dev/null +++ b/myapp/src/routes/+layout.svelte @@ -0,0 +1,6 @@ + + use-mcp SvelteKit Test + + + + diff --git a/myapp/src/routes/+page.svelte b/myapp/src/routes/+page.svelte new file mode 100644 index 0000000..24d0634 --- /dev/null +++ b/myapp/src/routes/+page.svelte @@ -0,0 +1,49 @@ + + +{#if mcp} + {#if $mcp.state === 'failed'} +

Connection failed: {$mcp.error}

+ + + {:else if $mcp.state !== 'ready'} +

Connecting to AI service… (state: {$mcp.state})

+ {:else} +

Available Tools: {$mcp.tools.length}

+ + {#if $mcp.tools.length > 0} +
    + {#each $mcp.tools as tool} +
  • +
    {tool.name}
    + {#if tool.description} +
    {tool.description}
    + {/if} +
    + +
    +
  • + {/each} +
+ {/if} + + {#if output} +
{output}
+ {/if} + {/if} +{:else} +

Loading…

+{/if} + diff --git a/myapp/src/routes/oauth/callback/+page.svelte b/myapp/src/routes/oauth/callback/+page.svelte new file mode 100644 index 0000000..e8c1e6c --- /dev/null +++ b/myapp/src/routes/oauth/callback/+page.svelte @@ -0,0 +1,9 @@ + + +

Authenticating…

+

This window should close automatically.

+ diff --git a/myapp/svelte.config.js b/myapp/svelte.config.js new file mode 100644 index 0000000..c47e6a7 --- /dev/null +++ b/myapp/svelte.config.js @@ -0,0 +1,13 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter() + } +}; + +export default config; + diff --git a/myapp/tsconfig.json b/myapp/tsconfig.json new file mode 100644 index 0000000..f4e5d10 --- /dev/null +++ b/myapp/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "types": ["svelte"] + } +} + diff --git a/myapp/vite.config.ts b/myapp/vite.config.ts new file mode 100644 index 0000000..c010b71 --- /dev/null +++ b/myapp/vite.config.ts @@ -0,0 +1,7 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +}); + From 2858b8a488d52213cd545a1a097f79cb1fbaed3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Mu=C5=A1tar?= Date: Fri, 5 Sep 2025 19:45:30 +0200 Subject: [PATCH 05/13] feat: SvelteKit test app inputs + Svelte adapter fix - myapp: re-add SvelteKit test app with tools list and per-tool input UI - Auto-generate inputs from JSON schema (string/number/boolean/enum + JSON fallback) - OAuth callback route and HTTP transport default - svelte adapter: keep internal stateRef synced with store state to avoid false "client not ready" errors when UI shows ready - chore: ignore myapp/.svelte-kit cache - chore: bump library to 0.1.2 (published) --- .gitignore | 4 ++ myapp/package.json | 3 +- myapp/src/lib/mcp.ts | 3 +- myapp/src/routes/+page.svelte | 104 ++++++++++++++++++++++++++++++---- package.json | 2 +- src/svelte/createMcp.ts | 7 ++- 6 files changed, 107 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 73d103a..d0a409e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ dist **/.claude/settings.local.json /public +# SvelteKit build cache for test app +myapp/.svelte-kit/ +my-app/.svelte-kit/ +myapp/pnpm-lock.yaml diff --git a/myapp/package.json b/myapp/package.json index 020df22..4c50675 100644 --- a/myapp/package.json +++ b/myapp/package.json @@ -21,7 +21,6 @@ "vite": "^7.1.4" }, "dependencies": { - "@gary149/use-mcp": "^0.1.1" + "@gary149/use-mcp": "^0.1.2" } } - diff --git a/myapp/src/lib/mcp.ts b/myapp/src/lib/mcp.ts index cf0c5f3..1334c9a 100644 --- a/myapp/src/lib/mcp.ts +++ b/myapp/src/lib/mcp.ts @@ -6,7 +6,6 @@ export const mcp = browser url: 'https://huggingface.co/mcp', clientName: 'My App', autoReconnect: true, - transportType: 'http' + transportType: 'http', }) : undefined - diff --git a/myapp/src/routes/+page.svelte b/myapp/src/routes/+page.svelte index 24d0634..db8c9fb 100644 --- a/myapp/src/routes/+page.svelte +++ b/myapp/src/routes/+page.svelte @@ -2,6 +2,37 @@ import { mcp } from '$lib/mcp' let output = '' + // Store per-tool argument objects + let argsByTool: Record = {} + let expanded: Record = {} + + function ensureArgs(name: string, schema: any) { + if (argsByTool[name]) return + const result: Record = {} + const properties = schema?.properties || {} + for (const key of Object.keys(properties)) { + const p = properties[key] || {} + if (p?.default !== undefined) { + result[key] = p.default + } else if (p?.type === 'boolean') { + result[key] = false + } else if (p?.type === 'number' || p?.type === 'integer') { + result[key] = 0 + } else if (p?.type === 'array') { + result[key] = [] + } else if (p?.type === 'object') { + result[key] = {} + } else { + result[key] = '' + } + } + argsByTool = { ...argsByTool, [name]: result } + } + + function setArg(toolName: string, key: string, value: any) { + argsByTool = { ...argsByTool, [toolName]: { ...(argsByTool[toolName] || {}), [key]: value } } + } + async function callTool(name: string, args: Record = {}) { if (!mcp) return try { @@ -26,15 +57,69 @@ {#if $mcp.tools.length > 0}
    {#each $mcp.tools as tool} -
  • -
    {tool.name}
    - {#if tool.description} -
    {tool.description}
    - {/if} -
    - -
    -
  • + {#key tool.name} +
  • +
    +
    +
    {tool.name}
    + {#if tool.description} +
    {tool.description}
    + {/if} +
    + +
    + + {#if expanded[tool.name]} + {@const schemaAny = (tool.inputSchema || tool.input_schema || { type:'object', properties:{} }) as any} + {#if schemaAny?.properties && Object.keys(schemaAny.properties).length > 0} +
    + {#each Object.entries(schemaAny.properties) as [key, prop]} + {#if (prop as any).enum} + + {:else if (prop as any).type === 'boolean'} + + {:else if (prop as any).type === 'number' || (prop as any).type === 'integer'} + + {:else if (prop as any).type === 'string'} + + {:else} + + {/if} + {/each} +
    + {:else} +
    No input parameters.
    + {/if} + +
    + +
    + {/if} +
  • + {/key} {/each}
{/if} @@ -46,4 +131,3 @@ {:else}

Loading…

{/if} - diff --git a/package.json b/package.json index 8f2d8e7..db2b35a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@gary149/use-mcp", "repository": "https://github.com/gary149/use-mcp", - "version": "0.1.0", + "version": "0.1.2", "license": "MIT", "type": "module", "files": [ diff --git a/src/svelte/createMcp.ts b/src/svelte/createMcp.ts index b3182a1..c4a6dda 100644 --- a/src/svelte/createMcp.ts +++ b/src/svelte/createMcp.ts @@ -117,6 +117,11 @@ export function createMcp(options: UseMcpOptions): Readable & Omit function setValue(patch: Partial) { current = { ...current, ...patch } + // Keep stateRef in sync whenever state is updated via setValue + if (typeof patch.state !== 'undefined') { + stateRef = patch.state as UseMcpResult['state'] + scheduleAutoRetryIfNeeded() + } set(current) } @@ -204,7 +209,7 @@ export function createMcp(options: UseMcpOptions): Readable & Omit addLog('debug', 'MCP Client initialized in connect.') } - const tryConnectWithTransport = async (kind: TransportType): Promise<'success' | 'fallback' | 'auth_redirect' | 'failed'> => { + const tryConnectWithTransport = async (kind: TransportType): Promise<'success' | 'auth_redirect' | 'failed'> => { addLog('info', `Attempting connection with ${kind.toUpperCase()} transport...`) if (stateRef !== 'authenticating') setState('connecting') From 6b9d03caf7290ae04a76d4f3167a5d9c16362d79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Mu=C5=A1tar?= Date: Fri, 5 Sep 2025 19:54:00 +0200 Subject: [PATCH 06/13] chore: remove use-mcp-svelte-scaffold; move myapp to examples/inspector-svelte and rename; update .gitignore and README --- .gitignore | 6 +++--- README.md | 16 +++++++++++++++- .../inspector-svelte}/package.json | 2 +- .../inspector-svelte}/src/app.d.ts | 0 .../inspector-svelte}/src/app.html | 0 .../inspector-svelte}/src/lib/mcp.ts | 0 .../inspector-svelte}/src/routes/+layout.svelte | 0 .../inspector-svelte}/src/routes/+page.svelte | 0 .../src/routes/oauth/callback/+page.svelte | 0 .../inspector-svelte}/svelte.config.js | 0 .../inspector-svelte}/tsconfig.json | 0 .../inspector-svelte}/vite.config.ts | 0 12 files changed, 19 insertions(+), 5 deletions(-) rename {myapp => examples/inspector-svelte}/package.json (95%) rename {myapp => examples/inspector-svelte}/src/app.d.ts (100%) rename {myapp => examples/inspector-svelte}/src/app.html (100%) rename {myapp => examples/inspector-svelte}/src/lib/mcp.ts (100%) rename {myapp => examples/inspector-svelte}/src/routes/+layout.svelte (100%) rename {myapp => examples/inspector-svelte}/src/routes/+page.svelte (100%) rename {myapp => examples/inspector-svelte}/src/routes/oauth/callback/+page.svelte (100%) rename {myapp => examples/inspector-svelte}/svelte.config.js (100%) rename {myapp => examples/inspector-svelte}/tsconfig.json (100%) rename {myapp => examples/inspector-svelte}/vite.config.ts (100%) diff --git a/.gitignore b/.gitignore index d0a409e..45dd063 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,6 @@ dist **/.claude/settings.local.json /public # SvelteKit build cache for test app -myapp/.svelte-kit/ -my-app/.svelte-kit/ -myapp/pnpm-lock.yaml +# Example app artifacts +examples/inspector-svelte/.svelte-kit/ +examples/inspector-svelte/pnpm-lock.yaml diff --git a/README.md b/README.md index 180bfb8..1ce3974 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,11 @@ A lightweight client for connecting to [Model Context Protocol (MCP)](https://gi Try it out: [Chat Demo](https://chat.use-mcp.dev) | [MCP Inspector](https://inspector.use-mcp.dev) | [Cloudflare Workers AI Playground](https://playground.ai.cloudflare.com/) +Examples in this repo: +- examples/chat-ui – React chat interface using `use-mcp` +- examples/inspector – React MCP inspector +- examples/inspector-svelte – SvelteKit inspector with per‑tool inputs + ## Installation ```bash @@ -22,7 +27,7 @@ yarn add @gary149/use-mcp ## Development -To run the development environment with all examples and servers: +To run the development environment with the React examples and servers: ```bash pnpm dev @@ -34,6 +39,15 @@ This starts: - **Hono MCP Server**: http://localhost:5101 - Example MCP server - **CF Agents MCP Server**: http://localhost:5102 - Cloudflare Workers AI MCP server +Svelte inspector example: + +```bash +cd examples/inspector-svelte +pnpm install +pnpm dev +``` +Then open the local URL shown by Vite and connect to an MCP server (e.g. https://huggingface.co/mcp). The UI lists tools and provides input fields generated from each tool's JSON schema. + ### Testing Integration tests are located in the `test/` directory and run headlessly by default: diff --git a/myapp/package.json b/examples/inspector-svelte/package.json similarity index 95% rename from myapp/package.json rename to examples/inspector-svelte/package.json index 4c50675..c47bbca 100644 --- a/myapp/package.json +++ b/examples/inspector-svelte/package.json @@ -1,5 +1,5 @@ { - "name": "myapp", + "name": "inspector-svelte", "private": true, "version": "0.0.1", "type": "module", diff --git a/myapp/src/app.d.ts b/examples/inspector-svelte/src/app.d.ts similarity index 100% rename from myapp/src/app.d.ts rename to examples/inspector-svelte/src/app.d.ts diff --git a/myapp/src/app.html b/examples/inspector-svelte/src/app.html similarity index 100% rename from myapp/src/app.html rename to examples/inspector-svelte/src/app.html diff --git a/myapp/src/lib/mcp.ts b/examples/inspector-svelte/src/lib/mcp.ts similarity index 100% rename from myapp/src/lib/mcp.ts rename to examples/inspector-svelte/src/lib/mcp.ts diff --git a/myapp/src/routes/+layout.svelte b/examples/inspector-svelte/src/routes/+layout.svelte similarity index 100% rename from myapp/src/routes/+layout.svelte rename to examples/inspector-svelte/src/routes/+layout.svelte diff --git a/myapp/src/routes/+page.svelte b/examples/inspector-svelte/src/routes/+page.svelte similarity index 100% rename from myapp/src/routes/+page.svelte rename to examples/inspector-svelte/src/routes/+page.svelte diff --git a/myapp/src/routes/oauth/callback/+page.svelte b/examples/inspector-svelte/src/routes/oauth/callback/+page.svelte similarity index 100% rename from myapp/src/routes/oauth/callback/+page.svelte rename to examples/inspector-svelte/src/routes/oauth/callback/+page.svelte diff --git a/myapp/svelte.config.js b/examples/inspector-svelte/svelte.config.js similarity index 100% rename from myapp/svelte.config.js rename to examples/inspector-svelte/svelte.config.js diff --git a/myapp/tsconfig.json b/examples/inspector-svelte/tsconfig.json similarity index 100% rename from myapp/tsconfig.json rename to examples/inspector-svelte/tsconfig.json diff --git a/myapp/vite.config.ts b/examples/inspector-svelte/vite.config.ts similarity index 100% rename from myapp/vite.config.ts rename to examples/inspector-svelte/vite.config.ts From 629c1e22610cab884f6c8b41c195834ea122e309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Mu=C5=A1tar?= Date: Fri, 5 Sep 2025 21:36:10 +0200 Subject: [PATCH 07/13] docs(README): update badges/links to gary149 repo and @gary149/use-mcp; link to examples/inspector-svelte --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1ce3974..cc93c89 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,16 @@ -[![GitHub last commit](https://img.shields.io/github/last-commit/modelcontextprotocol/use-mcp?logo=github&style=flat&label=​)](https://github.com/modelcontextprotocol/use-mcp)  [![npm](https://img.shields.io/npm/v/use-mcp?label=​&logo=npm)](https://www.npmjs.com/package/use-mcp) ![GitHub License](https://img.shields.io/github/license/modelcontextprotocol/use-mcp) +[![GitHub last commit](https://img.shields.io/github/last-commit/gary149/use-mcp?logo=github&style=flat&label=)](https://github.com/gary149/use-mcp)  [![npm](https://img.shields.io/npm/v/%40gary149%2Fuse-mcp?label=&logo=npm)](https://www.npmjs.com/package/@gary149/use-mcp) ![GitHub License](https://img.shields.io/github/license/gary149/use-mcp) A lightweight client for connecting to [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol) servers. Provides a React hook and a Svelte store adapter to simplify authentication and tool calling. Try it out: [Chat Demo](https://chat.use-mcp.dev) | [MCP Inspector](https://inspector.use-mcp.dev) | [Cloudflare Workers AI Playground](https://playground.ai.cloudflare.com/) Examples in this repo: -- examples/chat-ui – React chat interface using `use-mcp` -- examples/inspector – React MCP inspector -- examples/inspector-svelte – SvelteKit inspector with per‑tool inputs +- [examples/chat-ui](examples/chat-ui) – React chat interface using `@gary149/use-mcp` +- [examples/inspector](examples/inspector) – React MCP inspector +- [examples/inspector-svelte](examples/inspector-svelte) – SvelteKit inspector with per‑tool inputs ## Installation @@ -209,7 +209,7 @@ To handle the OAuth authentication flow, you need to set up a callback endpoint // App.tsx with React Router import { BrowserRouter as Router, Routes, Route } from 'react-router-dom' import { useEffect } from 'react' -import { onMcpAuthorization } from 'use-mcp' +import { onMcpAuthorization } from '@gary149/use-mcp' function OAuthCallback() { useEffect(() => { @@ -241,7 +241,7 @@ function App() { ```tsx // pages/oauth/callback.tsx import { useEffect } from 'react' -import { onMcpAuthorization } from 'use-mcp' +import { onMcpAuthorization } from '@gary149/use-mcp' export default function OAuthCallbackPage() { useEffect(() => { From e4e0153f7b381bd41d7baada7bfb626236e26181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Mu=C5=A1tar?= Date: Fri, 5 Sep 2025 22:50:53 +0200 Subject: [PATCH 08/13] feat(inspector-svelte): minimalist UI with Tailwind CDN, compact layout, always-visible inputs, inline results\n\nchore: bump version to 0.1.3 --- examples/inspector-svelte/src/app.html | 2 +- examples/inspector-svelte/src/lib/mcp.ts | 2 +- .../src/routes/+layout.svelte | 21 +- .../inspector-svelte/src/routes/+page.svelte | 190 ++++++++++++------ package.json | 2 +- 5 files changed, 144 insertions(+), 73 deletions(-) diff --git a/examples/inspector-svelte/src/app.html b/examples/inspector-svelte/src/app.html index c9835be..7cfc00c 100644 --- a/examples/inspector-svelte/src/app.html +++ b/examples/inspector-svelte/src/app.html @@ -3,6 +3,7 @@ + %sveltekit.head% @@ -10,4 +11,3 @@ - diff --git a/examples/inspector-svelte/src/lib/mcp.ts b/examples/inspector-svelte/src/lib/mcp.ts index 1334c9a..aa7fd83 100644 --- a/examples/inspector-svelte/src/lib/mcp.ts +++ b/examples/inspector-svelte/src/lib/mcp.ts @@ -3,7 +3,7 @@ import { createMcp } from '@gary149/use-mcp/svelte' export const mcp = browser ? createMcp({ - url: 'https://huggingface.co/mcp', + url: 'https://huggingface.co/mcp?login', clientName: 'My App', autoReconnect: true, transportType: 'http', diff --git a/examples/inspector-svelte/src/routes/+layout.svelte b/examples/inspector-svelte/src/routes/+layout.svelte index d875192..554a539 100644 --- a/examples/inspector-svelte/src/routes/+layout.svelte +++ b/examples/inspector-svelte/src/routes/+layout.svelte @@ -1,6 +1,21 @@ - use-mcp SvelteKit Test + MCP Inspector — Svelte - - +
+
+
+

MCP Inspector

+ Svelte + use-mcp +
+
+
+ +
+
diff --git a/examples/inspector-svelte/src/routes/+page.svelte b/examples/inspector-svelte/src/routes/+page.svelte index db8c9fb..78ee2fe 100644 --- a/examples/inspector-svelte/src/routes/+page.svelte +++ b/examples/inspector-svelte/src/routes/+page.svelte @@ -1,10 +1,13 @@ -{#if mcp} +{#if !mcp} +
Loading client…
+{:else} {#if $mcp.state === 'failed'} -

Connection failed: {$mcp.error}

- - +
+
Connection failed
+
{$mcp.error}
+
+ + +
+
{:else if $mcp.state !== 'ready'} -

Connecting to AI service… (state: {$mcp.state})

+
+
Connecting to AI service…
+
State: {$mcp.state}
+
{:else} -

Available Tools: {$mcp.tools.length}

+
+
+

Available Tools

+

{$mcp.tools.length} tool{ $mcp.tools.length === 1 ? '' : 's' } available

+
+
+ + +
+
{#if $mcp.tools.length > 0} -
    +
      {#each $mcp.tools as tool} {#key tool.name} -
    • -
      +
    • +
      -
      {tool.name}
      +
      {tool.name}
      {#if tool.description} -
      {tool.description}
      +
      {tool.description}
      {/if}
      -
      + +
      + {#if getSchema(tool)?.properties && Object.keys(getSchema(tool).properties).length > 0} +
      + {#each Object.entries(getSchema(tool).properties) as [key, prop]} + {#if (prop as any).enum} + + {:else if (prop as any).type === 'boolean'} + + {:else if (prop as any).type === 'number' || (prop as any).type === 'integer'} + + {:else if (prop as any).type === 'string'} + + {:else} + + {/if} + {/each} +
      + {:else} +
      No input parameters.
      + {/if} +
      + +
      + +
      - {#if expanded[tool.name]} - {@const schemaAny = (tool.inputSchema || tool.input_schema || { type:'object', properties:{} }) as any} - {#if schemaAny?.properties && Object.keys(schemaAny.properties).length > 0} -
      - {#each Object.entries(schemaAny.properties) as [key, prop]} - {#if (prop as any).enum} - - {:else if (prop as any).type === 'boolean'} - - {:else if (prop as any).type === 'number' || (prop as any).type === 'integer'} - - {:else if (prop as any).type === 'string'} - - {:else} - - {/if} - {/each} -
      - {:else} -
      No input parameters.
      - {/if} + {#if errorByTool[tool.name]} +
      + {errorByTool[tool.name]} +
      + {/if} -
      - + {#if resultsByTool[tool.name]} +
      +
      Result
      +
      {resultsByTool[tool.name]}
      {/if}
    • {/key} {/each}
    - {/if} - - {#if output} -
    {output}
    + {:else} +
    No tools available.
    {/if} {/if} -{:else} -

    Loading…

    {/if} diff --git a/package.json b/package.json index db2b35a..50f759b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@gary149/use-mcp", "repository": "https://github.com/gary149/use-mcp", - "version": "0.1.2", + "version": "0.1.3", "license": "MIT", "type": "module", "files": [ From 6df352610eb9fa017fce0664f3ccedfd16ac160e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Mu=C5=A1tar?= Date: Fri, 5 Sep 2025 22:51:20 +0200 Subject: [PATCH 09/13] chore(pkg): fix repository field via npm pkg fix --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 50f759b..da62761 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,9 @@ { "name": "@gary149/use-mcp", - "repository": "https://github.com/gary149/use-mcp", + "repository": { + "type": "git", + "url": "git+https://github.com/gary149/use-mcp.git" + }, "version": "0.1.3", "license": "MIT", "type": "module", From 90d0015282353637fe70714e3974ba33a2a05382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Mu=C5=A1tar?= Date: Fri, 5 Sep 2025 22:54:40 +0200 Subject: [PATCH 10/13] fix(inspector-svelte): update whitespace handling in results display --- examples/inspector-svelte/src/routes/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/inspector-svelte/src/routes/+page.svelte b/examples/inspector-svelte/src/routes/+page.svelte index 78ee2fe..7c21041 100644 --- a/examples/inspector-svelte/src/routes/+page.svelte +++ b/examples/inspector-svelte/src/routes/+page.svelte @@ -175,7 +175,7 @@ {#if resultsByTool[tool.name]}
    Result
    -
    {resultsByTool[tool.name]}
    +
    {resultsByTool[tool.name]}
    {/if} From 8bfe442286def33054320e43683c5b2d015e8ccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Mu=C5=A1tar?= Date: Fri, 5 Sep 2025 22:58:02 +0200 Subject: [PATCH 11/13] feat: rename package to @gary149/use-mcp-svelte; Svelte-first README; update examples imports\n\nchore: bump version to 0.1.4 --- README.md | 104 +++++++++--------- examples/inspector-svelte/package.json | 2 +- examples/inspector-svelte/src/lib/mcp.ts | 2 +- .../src/routes/oauth/callback/+page.svelte | 3 +- package.json | 10 +- 5 files changed, 60 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index cc93c89..20a8c4a 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,28 @@
    -# 🦑 use-mcp 🦑 +# 🦑 use-mcp-svelte 🦑
    -[![GitHub last commit](https://img.shields.io/github/last-commit/gary149/use-mcp?logo=github&style=flat&label=)](https://github.com/gary149/use-mcp)  [![npm](https://img.shields.io/npm/v/%40gary149%2Fuse-mcp?label=&logo=npm)](https://www.npmjs.com/package/@gary149/use-mcp) ![GitHub License](https://img.shields.io/github/license/gary149/use-mcp) +[![GitHub last commit](https://img.shields.io/github/last-commit/gary149/use-mcp-svelte?logo=github&style=flat&label=)](https://github.com/gary149/use-mcp-svelte)  [![npm](https://img.shields.io/npm/v/%40gary149%2Fuse-mcp-svelte?label=&logo=npm)](https://www.npmjs.com/package/@gary149/use-mcp-svelte) ![GitHub License](https://img.shields.io/github/license/gary149/use-mcp-svelte) -A lightweight client for connecting to [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol) servers. Provides a React hook and a Svelte store adapter to simplify authentication and tool calling. +A lightweight client for connecting to [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol) servers. Focused on Svelte with a clean store adapter; React bindings are also available. Try it out: [Chat Demo](https://chat.use-mcp.dev) | [MCP Inspector](https://inspector.use-mcp.dev) | [Cloudflare Workers AI Playground](https://playground.ai.cloudflare.com/) -Examples in this repo: -- [examples/chat-ui](examples/chat-ui) – React chat interface using `@gary149/use-mcp` -- [examples/inspector](examples/inspector) – React MCP inspector +Examples in this repo (Svelte first): - [examples/inspector-svelte](examples/inspector-svelte) – SvelteKit inspector with per‑tool inputs +- [examples/chat-ui](examples/chat-ui) – React chat interface using `@gary149/use-mcp-svelte/react` +- [examples/inspector](examples/inspector) – React MCP inspector -## Installation +## Installation (Svelte) ```bash -npm install @gary149/use-mcp +npm install @gary149/use-mcp-svelte # or -pnpm add @gary149/use-mcp +pnpm add @gary149/use-mcp-svelte # or -yarn add @gary149/use-mcp +yarn add @gary149/use-mcp-svelte ``` ## Development @@ -71,10 +71,48 @@ cd test && pnpm test:ui # Run tests with interactive UI - 📝 Comprehensive logging for debugging - 🌐 Works with both HTTP and SSE (Server-Sent Events) transports (HTTP streaming recommended) +## Quick Start (Svelte/SvelteKit) +```ts +// src/lib/mcp.ts +import { browser } from '$app/environment' +import { createMcp } from '@gary149/use-mcp-svelte/svelte' + +export const mcp = browser ? createMcp({ + url: 'https://your-mcp-server.com', + clientName: 'My App', + autoReconnect: true, + // transportType: 'http', // recommended; SSE is legacy +}) : undefined +``` + +```svelte + + + +{#if mcp} + {#if $mcp.state === 'failed'} +

    Connection failed: {$mcp.error}

    + + + {:else if $mcp.state !== 'ready'} +

    Connecting to AI service…

    + {:else} +

    Available Tools: {$mcp.tools.length}

    + + {/if} +{:else} +

    Loading…

    +{/if} +``` + ## Quick Start (React) ```tsx -import { useMcp } from '@gary149/use-mcp/react' +import { useMcp } from '@gary149/use-mcp-svelte/react' function MyAIComponent() { const { @@ -160,44 +198,6 @@ function MyAIComponent() { } ``` -## Quick Start (Svelte/SvelteKit) - -```ts -// src/lib/mcp.ts -import { browser } from '$app/environment' -import { createMcp } from '@gary149/use-mcp/svelte' - -export const mcp = browser ? createMcp({ - url: 'https://your-mcp-server.com', - clientName: 'My App', - autoReconnect: true, - // transportType: 'http', // recommended; SSE is legacy -}) : undefined -``` - -```svelte - - - -{#if mcp} - {#if $mcp.state === 'failed'} -

    Connection failed: {$mcp.error}

    - - - {:else if $mcp.state !== 'ready'} -

    Connecting to AI service…

    - {:else} -

    Available Tools: {$mcp.tools.length}

    - - {/if} -{:else} -

    Loading…

    -{/if} -``` ## Setting Up OAuth Callback @@ -209,7 +209,7 @@ To handle the OAuth authentication flow, you need to set up a callback endpoint // App.tsx with React Router import { BrowserRouter as Router, Routes, Route } from 'react-router-dom' import { useEffect } from 'react' -import { onMcpAuthorization } from '@gary149/use-mcp' +import { onMcpAuthorization } from '@gary149/use-mcp-svelte' function OAuthCallback() { useEffect(() => { @@ -241,7 +241,7 @@ function App() { ```tsx // pages/oauth/callback.tsx import { useEffect } from 'react' -import { onMcpAuthorization } from '@gary149/use-mcp' +import { onMcpAuthorization } from '@gary149/use-mcp-svelte' export default function OAuthCallbackPage() { useEffect(() => { @@ -263,7 +263,7 @@ export default function OAuthCallbackPage() { diff --git a/examples/inspector-svelte/package.json b/examples/inspector-svelte/package.json index c47bbca..6b48e36 100644 --- a/examples/inspector-svelte/package.json +++ b/examples/inspector-svelte/package.json @@ -21,6 +21,6 @@ "vite": "^7.1.4" }, "dependencies": { - "@gary149/use-mcp": "^0.1.2" + "@gary149/use-mcp-svelte": "^0.1.4" } } diff --git a/examples/inspector-svelte/src/lib/mcp.ts b/examples/inspector-svelte/src/lib/mcp.ts index aa7fd83..fec2f31 100644 --- a/examples/inspector-svelte/src/lib/mcp.ts +++ b/examples/inspector-svelte/src/lib/mcp.ts @@ -1,5 +1,5 @@ import { browser } from '$app/environment' -import { createMcp } from '@gary149/use-mcp/svelte' +import { createMcp } from '@gary149/use-mcp-svelte/svelte' export const mcp = browser ? createMcp({ diff --git a/examples/inspector-svelte/src/routes/oauth/callback/+page.svelte b/examples/inspector-svelte/src/routes/oauth/callback/+page.svelte index e8c1e6c..bc988ed 100644 --- a/examples/inspector-svelte/src/routes/oauth/callback/+page.svelte +++ b/examples/inspector-svelte/src/routes/oauth/callback/+page.svelte @@ -1,9 +1,8 @@

    Authenticating…

    This window should close automatically.

    - diff --git a/package.json b/package.json index da62761..82ad256 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { - "name": "@gary149/use-mcp", + "name": "@gary149/use-mcp-svelte", "repository": { "type": "git", - "url": "git+https://github.com/gary149/use-mcp.git" + "url": "git+https://github.com/gary149/use-mcp-svelte.git" }, - "version": "0.1.3", + "version": "0.1.4", "license": "MIT", "type": "module", "files": [ @@ -31,9 +31,9 @@ }, "svelte": "./dist/svelte/index.js", "sideEffects": false, - "homepage": "https://github.com/gary149/use-mcp#readme", + "homepage": "https://github.com/gary149/use-mcp-svelte#readme", "bugs": { - "url": "https://github.com/gary149/use-mcp/issues" + "url": "https://github.com/gary149/use-mcp-svelte/issues" }, "scripts": { "install:all": "concurrently 'pnpm install' 'cd examples/chat-ui && pnpm install' 'cd examples/inspector && pnpm install' 'cd examples/servers/hono-mcp && pnpm install' 'cd examples/servers/cf-agents && pnpm install'", From 7e12b4f0f6d3aafeded5558ebeab19e6af8ec26a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Mu=C5=A1tar?= Date: Fri, 5 Sep 2025 23:01:11 +0200 Subject: [PATCH 12/13] Revert "feat: rename package to @gary149/use-mcp-svelte; Svelte-first README; update examples imports\n\nchore: bump version to 0.1.4" This reverts commit 8bfe442286def33054320e43683c5b2d015e8ccd. --- README.md | 104 +++++++++--------- examples/inspector-svelte/package.json | 2 +- examples/inspector-svelte/src/lib/mcp.ts | 2 +- .../src/routes/oauth/callback/+page.svelte | 3 +- package.json | 10 +- 5 files changed, 61 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 20a8c4a..cc93c89 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,28 @@
    -# 🦑 use-mcp-svelte 🦑 +# 🦑 use-mcp 🦑
    -[![GitHub last commit](https://img.shields.io/github/last-commit/gary149/use-mcp-svelte?logo=github&style=flat&label=)](https://github.com/gary149/use-mcp-svelte)  [![npm](https://img.shields.io/npm/v/%40gary149%2Fuse-mcp-svelte?label=&logo=npm)](https://www.npmjs.com/package/@gary149/use-mcp-svelte) ![GitHub License](https://img.shields.io/github/license/gary149/use-mcp-svelte) +[![GitHub last commit](https://img.shields.io/github/last-commit/gary149/use-mcp?logo=github&style=flat&label=)](https://github.com/gary149/use-mcp)  [![npm](https://img.shields.io/npm/v/%40gary149%2Fuse-mcp?label=&logo=npm)](https://www.npmjs.com/package/@gary149/use-mcp) ![GitHub License](https://img.shields.io/github/license/gary149/use-mcp) -A lightweight client for connecting to [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol) servers. Focused on Svelte with a clean store adapter; React bindings are also available. +A lightweight client for connecting to [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol) servers. Provides a React hook and a Svelte store adapter to simplify authentication and tool calling. Try it out: [Chat Demo](https://chat.use-mcp.dev) | [MCP Inspector](https://inspector.use-mcp.dev) | [Cloudflare Workers AI Playground](https://playground.ai.cloudflare.com/) -Examples in this repo (Svelte first): -- [examples/inspector-svelte](examples/inspector-svelte) – SvelteKit inspector with per‑tool inputs -- [examples/chat-ui](examples/chat-ui) – React chat interface using `@gary149/use-mcp-svelte/react` +Examples in this repo: +- [examples/chat-ui](examples/chat-ui) – React chat interface using `@gary149/use-mcp` - [examples/inspector](examples/inspector) – React MCP inspector +- [examples/inspector-svelte](examples/inspector-svelte) – SvelteKit inspector with per‑tool inputs -## Installation (Svelte) +## Installation ```bash -npm install @gary149/use-mcp-svelte +npm install @gary149/use-mcp # or -pnpm add @gary149/use-mcp-svelte +pnpm add @gary149/use-mcp # or -yarn add @gary149/use-mcp-svelte +yarn add @gary149/use-mcp ``` ## Development @@ -71,48 +71,10 @@ cd test && pnpm test:ui # Run tests with interactive UI - 📝 Comprehensive logging for debugging - 🌐 Works with both HTTP and SSE (Server-Sent Events) transports (HTTP streaming recommended) -## Quick Start (Svelte/SvelteKit) -```ts -// src/lib/mcp.ts -import { browser } from '$app/environment' -import { createMcp } from '@gary149/use-mcp-svelte/svelte' - -export const mcp = browser ? createMcp({ - url: 'https://your-mcp-server.com', - clientName: 'My App', - autoReconnect: true, - // transportType: 'http', // recommended; SSE is legacy -}) : undefined -``` - -```svelte - - - -{#if mcp} - {#if $mcp.state === 'failed'} -

    Connection failed: {$mcp.error}

    - - - {:else if $mcp.state !== 'ready'} -

    Connecting to AI service…

    - {:else} -

    Available Tools: {$mcp.tools.length}

    - - {/if} -{:else} -

    Loading…

    -{/if} -``` - ## Quick Start (React) ```tsx -import { useMcp } from '@gary149/use-mcp-svelte/react' +import { useMcp } from '@gary149/use-mcp/react' function MyAIComponent() { const { @@ -198,6 +160,44 @@ function MyAIComponent() { } ``` +## Quick Start (Svelte/SvelteKit) + +```ts +// src/lib/mcp.ts +import { browser } from '$app/environment' +import { createMcp } from '@gary149/use-mcp/svelte' + +export const mcp = browser ? createMcp({ + url: 'https://your-mcp-server.com', + clientName: 'My App', + autoReconnect: true, + // transportType: 'http', // recommended; SSE is legacy +}) : undefined +``` + +```svelte + + + +{#if mcp} + {#if $mcp.state === 'failed'} +

    Connection failed: {$mcp.error}

    + + + {:else if $mcp.state !== 'ready'} +

    Connecting to AI service…

    + {:else} +

    Available Tools: {$mcp.tools.length}

    + + {/if} +{:else} +

    Loading…

    +{/if} +``` ## Setting Up OAuth Callback @@ -209,7 +209,7 @@ To handle the OAuth authentication flow, you need to set up a callback endpoint // App.tsx with React Router import { BrowserRouter as Router, Routes, Route } from 'react-router-dom' import { useEffect } from 'react' -import { onMcpAuthorization } from '@gary149/use-mcp-svelte' +import { onMcpAuthorization } from '@gary149/use-mcp' function OAuthCallback() { useEffect(() => { @@ -241,7 +241,7 @@ function App() { ```tsx // pages/oauth/callback.tsx import { useEffect } from 'react' -import { onMcpAuthorization } from '@gary149/use-mcp-svelte' +import { onMcpAuthorization } from '@gary149/use-mcp' export default function OAuthCallbackPage() { useEffect(() => { @@ -263,7 +263,7 @@ export default function OAuthCallbackPage() { diff --git a/examples/inspector-svelte/package.json b/examples/inspector-svelte/package.json index 6b48e36..c47bbca 100644 --- a/examples/inspector-svelte/package.json +++ b/examples/inspector-svelte/package.json @@ -21,6 +21,6 @@ "vite": "^7.1.4" }, "dependencies": { - "@gary149/use-mcp-svelte": "^0.1.4" + "@gary149/use-mcp": "^0.1.2" } } diff --git a/examples/inspector-svelte/src/lib/mcp.ts b/examples/inspector-svelte/src/lib/mcp.ts index fec2f31..aa7fd83 100644 --- a/examples/inspector-svelte/src/lib/mcp.ts +++ b/examples/inspector-svelte/src/lib/mcp.ts @@ -1,5 +1,5 @@ import { browser } from '$app/environment' -import { createMcp } from '@gary149/use-mcp-svelte/svelte' +import { createMcp } from '@gary149/use-mcp/svelte' export const mcp = browser ? createMcp({ diff --git a/examples/inspector-svelte/src/routes/oauth/callback/+page.svelte b/examples/inspector-svelte/src/routes/oauth/callback/+page.svelte index bc988ed..e8c1e6c 100644 --- a/examples/inspector-svelte/src/routes/oauth/callback/+page.svelte +++ b/examples/inspector-svelte/src/routes/oauth/callback/+page.svelte @@ -1,8 +1,9 @@

    Authenticating…

    This window should close automatically.

    + diff --git a/package.json b/package.json index 82ad256..da62761 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { - "name": "@gary149/use-mcp-svelte", + "name": "@gary149/use-mcp", "repository": { "type": "git", - "url": "git+https://github.com/gary149/use-mcp-svelte.git" + "url": "git+https://github.com/gary149/use-mcp.git" }, - "version": "0.1.4", + "version": "0.1.3", "license": "MIT", "type": "module", "files": [ @@ -31,9 +31,9 @@ }, "svelte": "./dist/svelte/index.js", "sideEffects": false, - "homepage": "https://github.com/gary149/use-mcp-svelte#readme", + "homepage": "https://github.com/gary149/use-mcp#readme", "bugs": { - "url": "https://github.com/gary149/use-mcp-svelte/issues" + "url": "https://github.com/gary149/use-mcp/issues" }, "scripts": { "install:all": "concurrently 'pnpm install' 'cd examples/chat-ui && pnpm install' 'cd examples/inspector && pnpm install' 'cd examples/servers/hono-mcp && pnpm install' 'cd examples/servers/cf-agents && pnpm install'", From 6a0c7bb7def351440df3843067b285b35bf4a6a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Mu=C5=A1tar?= Date: Fri, 5 Sep 2025 23:09:40 +0200 Subject: [PATCH 13/13] feat: add note about Svelte support and usage in README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index cc93c89..72fa87e 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ +> [!NOTE] +> This package includes Svelte support. Use the Svelte adapter via `@gary149/use-mcp/svelte` and see the Svelte inspector example in `examples/inspector-svelte`. + [![GitHub last commit](https://img.shields.io/github/last-commit/gary149/use-mcp?logo=github&style=flat&label=)](https://github.com/gary149/use-mcp)  [![npm](https://img.shields.io/npm/v/%40gary149%2Fuse-mcp?label=&logo=npm)](https://www.npmjs.com/package/@gary149/use-mcp) ![GitHub License](https://img.shields.io/github/license/gary149/use-mcp) A lightweight client for connecting to [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol) servers. Provides a React hook and a Svelte store adapter to simplify authentication and tool calling.