diff --git a/.gitignore b/.gitignore
index 73d103a..45dd063 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,7 @@ dist
**/.claude/settings.local.json
/public
+# SvelteKit build cache for test app
+# Example app artifacts
+examples/inspector-svelte/.svelte-kit/
+examples/inspector-svelte/pnpm-lock.yaml
diff --git a/README.md b/README.md
index b247654..72fa87e 100644
--- a/README.md
+++ b/README.md
@@ -4,25 +4,33 @@
-[](https://github.com/modelcontextprotocol/use-mcp) [](https://www.npmjs.com/package/use-mcp) 
+> [!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`.
-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.
+[](https://github.com/gary149/use-mcp) [](https://www.npmjs.com/package/@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](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
```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
-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 +42,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:
@@ -49,18 +66,18 @@ 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'
+import { useMcp } from '@gary149/use-mcp/react'
function MyAIComponent() {
const {
@@ -146,6 +163,45 @@ 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}
+ mcp.retry()}>Retry
+ mcp.authenticate()}>Authenticate Manually
+ {:else if $mcp.state !== 'ready'}
+ Connecting to AI service…
+ {:else}
+ Available Tools: {$mcp.tools.length}
+ mcp.callTool('search', { query: 'example search' })}>
+ Search
+
+ {/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.
@@ -156,7 +212,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(() => {
@@ -188,7 +244,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(() => {
@@ -204,6 +260,22 @@ export default function OAuthCallbackPage() {
}
```
+### With SvelteKit
+
+```svelte
+
+
+
+Authenticating…
+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
@@ -253,4 +325,4 @@ function useMcp(options: UseMcpOptions): UseMcpResult
## License
-MIT
\ No newline at end of file
+MIT
diff --git a/examples/inspector-svelte/package.json b/examples/inspector-svelte/package.json
new file mode 100644
index 0000000..c47bbca
--- /dev/null
+++ b/examples/inspector-svelte/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "inspector-svelte",
+ "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.2"
+ }
+}
diff --git a/examples/inspector-svelte/src/app.d.ts b/examples/inspector-svelte/src/app.d.ts
new file mode 100644
index 0000000..8cb1955
--- /dev/null
+++ b/examples/inspector-svelte/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/examples/inspector-svelte/src/app.html b/examples/inspector-svelte/src/app.html
new file mode 100644
index 0000000..7cfc00c
--- /dev/null
+++ b/examples/inspector-svelte/src/app.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
+
+
diff --git a/examples/inspector-svelte/src/lib/mcp.ts b/examples/inspector-svelte/src/lib/mcp.ts
new file mode 100644
index 0000000..aa7fd83
--- /dev/null
+++ b/examples/inspector-svelte/src/lib/mcp.ts
@@ -0,0 +1,11 @@
+import { browser } from '$app/environment'
+import { createMcp } from '@gary149/use-mcp/svelte'
+
+export const mcp = browser
+ ? createMcp({
+ url: 'https://huggingface.co/mcp?login',
+ clientName: 'My App',
+ autoReconnect: true,
+ transportType: 'http',
+ })
+ : undefined
diff --git a/examples/inspector-svelte/src/routes/+layout.svelte b/examples/inspector-svelte/src/routes/+layout.svelte
new file mode 100644
index 0000000..554a539
--- /dev/null
+++ b/examples/inspector-svelte/src/routes/+layout.svelte
@@ -0,0 +1,21 @@
+
+ MCP Inspector — Svelte
+
+
+
diff --git a/examples/inspector-svelte/src/routes/+page.svelte b/examples/inspector-svelte/src/routes/+page.svelte
new file mode 100644
index 0000000..7c21041
--- /dev/null
+++ b/examples/inspector-svelte/src/routes/+page.svelte
@@ -0,0 +1,189 @@
+
+
+{#if !mcp}
+ Loading client…
+{:else}
+ {#if $mcp.state === 'failed'}
+
+
Connection failed
+
{$mcp.error}
+
+ mcp.retry()}>Retry
+ mcp.authenticate()}>Authenticate
+
+
+ {:else if $mcp.state !== 'ready'}
+
+
Connecting to AI service…
+
State: {$mcp.state}
+
+ {:else}
+
+
+
Available Tools
+
{$mcp.tools.length} tool{ $mcp.tools.length === 1 ? '' : 's' } available
+
+
+ mcp.authenticate()}>Authenticate
+ mcp.retry()}>Reconnect
+
+
+
+ {#if $mcp.tools.length > 0}
+
+ {#each $mcp.tools as tool}
+ {#key tool.name}
+
+
+
+
{tool.name}
+ {#if tool.description}
+
{tool.description}
+ {/if}
+
+
+
+
+
+
+ callTool(tool.name, argsByTool[tool.name] || {})}>
+ {#if loadingByTool[tool.name]}Calling…{:else}Call Tool{/if}
+
+ { resultsByTool = { ...resultsByTool, [tool.name]: '' }; errorByTool = { ...errorByTool, [tool.name]: '' } }}>
+ Clear
+
+
+
+ {#if errorByTool[tool.name]}
+
+ {errorByTool[tool.name]}
+
+ {/if}
+
+ {#if resultsByTool[tool.name]}
+
+
Result
+
{resultsByTool[tool.name]}
+
+ {/if}
+
+ {/key}
+ {/each}
+
+ {:else}
+ No tools available.
+ {/if}
+ {/if}
+{/if}
diff --git a/examples/inspector-svelte/src/routes/oauth/callback/+page.svelte b/examples/inspector-svelte/src/routes/oauth/callback/+page.svelte
new file mode 100644
index 0000000..e8c1e6c
--- /dev/null
+++ b/examples/inspector-svelte/src/routes/oauth/callback/+page.svelte
@@ -0,0 +1,9 @@
+
+
+Authenticating…
+This window should close automatically.
+
diff --git a/examples/inspector-svelte/svelte.config.js b/examples/inspector-svelte/svelte.config.js
new file mode 100644
index 0000000..c47e6a7
--- /dev/null
+++ b/examples/inspector-svelte/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/examples/inspector-svelte/tsconfig.json b/examples/inspector-svelte/tsconfig.json
new file mode 100644
index 0000000..f4e5d10
--- /dev/null
+++ b/examples/inspector-svelte/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "types": ["svelte"]
+ }
+}
+
diff --git a/examples/inspector-svelte/vite.config.ts b/examples/inspector-svelte/vite.config.ts
new file mode 100644
index 0000000..c010b71
--- /dev/null
+++ b/examples/inspector-svelte/vite.config.ts
@@ -0,0 +1,7 @@
+import { sveltekit } from '@sveltejs/kit/vite';
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ plugins: [sveltekit()]
+});
+
diff --git a/package.json b/package.json
index 6a7cd6b..da62761 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,11 @@
{
- "name": "use-mcp",
- "repository": "https://github.com/modelcontextprotocol/use-mcp",
- "version": "0.0.21",
+ "name": "@gary149/use-mcp",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/gary149/use-mcp.git"
+ },
+ "version": "0.1.3",
+ "license": "MIT",
"type": "module",
"files": [
"dist",
@@ -18,14 +22,26 @@
"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"
}
},
+ "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",
@@ -33,6 +49,9 @@
"deploy:site": "npx wrangler deploy",
"prepare": "husky"
},
+ "publishConfig": {
+ "access": "public"
+ },
"dependencies": {
"@modelcontextprotocol/sdk": "^1.13.3",
"strict-url-sanitise": "^0.0.1"
@@ -44,15 +63,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 +86,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/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
new file mode 100644
index 0000000..c4a6dda
--- /dev/null
+++ b/src/svelte/createMcp.ts
@@ -0,0 +1,640 @@
+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 } 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,
+ allowedOrigins = [],
+ } = 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
+ 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 }
+ // Keep stateRef in sync whenever state is updated via setValue
+ if (typeof patch.state !== 'undefined') {
+ stateRef = patch.state as UseMcpResult['state']
+ scheduleAutoRetryIfNeeded()
+ }
+ 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 ${serverUrl}...`)
+
+ // init provider/client
+ if (!authProvider) {
+ authProvider = new BrowserOAuthClientProvider(serverUrl, {
+ 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' | '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 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, {
+ ...baseOpts,
+ requestInit: { headers: { ...customHeaders } },
+ })
+ } else {
+ addLog('debug', 'Creating SSEClientTransport...')
+ transportInstance = new SSEClientTransport(targetUrl, {
+ ...baseOpts,
+ requestInit: { headers: { Accept: 'text/event-stream', ...customHeaders } },
+ })
+ }
+ 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 })
+ 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' = 'failed'
+ if (transportType === 'sse') {
+ finalStatus = await tryConnectWithTransport('sse')
+ } else if (transportType === 'http') {
+ finalStatus = await tryConnectWithTransport('http')
+ } else {
+ const httpResult = await tryConnectWithTransport('http')
+ if (httpResult !== 'success' && httpResult !== 'auth_redirect') {
+ addLog('info', 'HTTP connect 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(() => {
+ 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 })
+ 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(() => {
+ 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 })
+ 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 ${serverUrl}.`)
+ 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 !== serverUrl) {
+ authProvider = new BrowserOAuthClientProvider(serverUrl, {
+ 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 && !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)
+ 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,
+ })
+
+ // 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
+}
+
+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..55f5e2c
--- /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'
+export { onMcpAuthorization } from '../auth/callback.js'
+
+// Re-export core types for convenience
+export type { Tool, Resource, ResourceTemplate, Prompt } from '@modelcontextprotocol/sdk/types.js'