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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
410 changes: 52 additions & 358 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/PERFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ MCP servers (like `@youdotcom-oss/mcp`) have higher thresholds (100ms/50%/400KB)
**MCP Server Overhead Sources**:
- **Stdio transport**: Process IPC adds 20-40ms latency
- **JSON-RPC protocol**: Serialization/deserialization adds 10-20ms
- **Client SDK**: `@modelcontextprotocol/sdk` protocol overhead
- **Client SDK**: `@modelcontextprotocol/client` protocol overhead
- **Process spawning**: Each test spawns MCP server as subprocess
- **State management**: Client state, connection pools, schemas

Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@
]
},
"devDependencies": {
"@biomejs/biome": "2.5.4",
"@biomejs/biome": "2.5.9",
"@commitlint/cli": "^21.2.1",
"@commitlint/config-conventional": "^21.2.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@modelcontextprotocol/client": "^2.0.0",
"@types/bun": "latest",
"format-package": "^8.0.0",
"lint-staged": "^17.1.0",
"typescript": "7.0.2",
"typescript-language-server": "5.3.0",
"zod": "^4.3.6"
"zod": "^4.4.3"
}
}
8 changes: 5 additions & 3 deletions packages/ai-sdk-plugin/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Vercel AI SDK Plugin for You.com

Give your AI applications **real-time access to the web** through the hosted You.com MCP server. This package exposes an async `createYouClient()` helper that connects to `https://api.you.com/mcp` and returns the underlying AI SDK MCP client. By default, `await client.tools()` resolves to the default hosted tool set: `you-search`, `you-contents`, `you-research`, `you-balance`, and `you-discover`.
Give your AI applications **real-time access to the web** through the hosted You.com MCP server. This package exposes an async `createYouClient()` helper that connects to `https://api.you.com/mcp` and returns the underlying AI SDK MCP client. By default, `await client.tools()` resolves to the default hosted tool set: `you-search`, `you-contents`, `you-research`, `you-balance`, `you-discover`, and `you-answer`.

## Features

Expand Down Expand Up @@ -85,7 +85,7 @@ try {
}
```

`createYouClient()` returns the underlying MCP client. Call `await client.tools()` to resolve the default hosted tool set (`you-search`, `you-contents`, `you-research`, `you-balance`, and `you-discover`) unless you scope it with `tools`, and call `await client.close()` when finished.
`createYouClient()` returns the underlying MCP client. Call `await client.tools()` to resolve the default hosted tool set (`you-search`, `you-contents`, `you-research`, `you-balance`, `you-discover`, and `you-answer`) unless you scope it with `tools`, and call `await client.close()` when finished.

Set your You.com API key as an environment variable:

Expand Down Expand Up @@ -222,6 +222,7 @@ The default hosted tool set is:
- `you-research`
- `you-balance`
- `you-discover`
- `you-answer`

Optional tools:

Expand All @@ -233,7 +234,7 @@ Optional tools:

`profile` selects a hosted server profile. `tools` scopes which tools are visible. If `profile` is provided, it takes precedence over `tools`.

Today, `profile: 'free'` is a search-only mode. It overrides `tools` and does not expose `you-contents`, `you-research`, `you-finance`, `you-balance`, `you-discover`, or livecrawl.
Today, `profile: 'free'` is a search-only mode. It overrides `tools` and does not expose `you-contents`, `you-research`, `you-finance`, `you-balance`, `you-discover`, `you-answer`, or livecrawl.

### Using different model providers

Expand Down Expand Up @@ -309,6 +310,7 @@ Default tools:
- `you-research`
- `you-balance`
- `you-discover`
- `you-answer`

Optional tools:

Expand Down
9 changes: 4 additions & 5 deletions packages/ai-sdk-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,14 @@
},
"types": "./dist/main.d.ts",
"dependencies": {
"@ai-sdk/mcp": "^1.0.42",
"@modelcontextprotocol/sdk": "^1.29.0"
"@ai-sdk/mcp": "^2.0.34"
},
"peerDependencies": {
"ai": "^6.0.168"
"ai": "^7.0.0"
},
"devDependencies": {
"@ai-sdk/anthropic": "^3.0.71",
"@ai-sdk/anthropic": "^4.0.40",
"@types/bun": "latest",
"ai": "^6.0.168"
"ai": "^7.0.70"
}
}
7 changes: 7 additions & 0 deletions packages/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ export YDC_ALLOWED_TOOLS="you-search,you-research,you-contents"

Explicit `allowedTools` takes precedence over `YDC_ALLOWED_TOOLS`.

When neither `allowedTools` nor `YDC_ALLOWED_TOOLS` is provided and no `profile`
is set, the package advertises every known tool by default
(`?tools=<all known tool names>`). This keeps the typed surface truthful — every
`KnownToolName` is callable on a default connection, including `you-finance`
(which the server only exposes via an explicit `?tools=` filter). Pass
`allowedTools` or a `profile` to narrow the set.

### Profiles and auth

Set `profile` to route to a hosted profile (sent as `?profile=...`):
Expand Down
5 changes: 3 additions & 2 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
},
"scripts": {
"build": "bun run build:bundle && bun run build:types",
"build:bundle": "bun build src/main.ts --outdir dist --target node --external @modelcontextprotocol/sdk",
"build:bundle": "bun build src/main.ts --outdir dist --target node --external @modelcontextprotocol/client",
"build:types": "tsc --project tsconfig.build.json --declaration --emitDeclarationOnly --noEmit false",
"check": "bun run check:biome && bun run check:types && bun run check:package",
"check:biome": "biome check",
Expand All @@ -56,9 +56,10 @@
},
"types": "./dist/main.d.ts",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0"
"@modelcontextprotocol/client": "^2.0.0"
},
"devDependencies": {
"@modelcontextprotocol/server": "^2.0.0",
"@types/bun": "latest"
}
}
30 changes: 24 additions & 6 deletions packages/api/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'
import packageJson from '../package.json' with { type: 'json' }
import type { KnownToolInput, KnownToolName, KnownToolOutput } from './tool-schemas.ts'
import {
API_TOOL_SCHEMA_HASH,
API_TOOL_SCHEMAS,
type KnownToolInput,
type KnownToolName,
type KnownToolOutput,
} from './tool-schemas.ts'

export type {
KnownToolInput,
KnownToolName,
KnownToolOutput,
YouBalanceInput,
YouBalanceOutput,
YouAnswerInput,
YouAnswerOutput,
YouContentsInput,
YouContentsOutput,
YouDiscoverInput,
YouDiscoverOutput,
YouFinanceInput,
YouFinanceOutput,
YouResearchInput,
YouResearchOutput,
YouSearchInput,
YouSearchOutput,
} from './tool-schemas.ts'
export { API_TOOL_SCHEMA_HASH, API_TOOL_SCHEMAS } from './tool-schemas.ts'
export { API_TOOL_SCHEMA_HASH, API_TOOL_SCHEMAS }

const BASE_MCP_SERVER_URL = 'https://api.you.com/mcp'

Expand All @@ -41,8 +50,17 @@ const buildMcpUrl = ({ allowedTools, profile }: Pick<YouApiConfig, 'allowedTools
: undefined
: allowedTools || process.env.YDC_ALLOWED_TOOLS

// The api package is consumed imperatively (a caller wires specific tool
// calls), not by an agent browsing a tool list. So a connection with no
// explicit scope advertises every known tool by default — keeping the typed
// surface truthful, since every `KnownToolName` (including `you-finance`,
// which the server only exposes via an explicit `?tools=` filter) is callable
// on a default connection. A `profile` already scopes the tool set, so it is
// left to define the ceiling without an explicit `?tools=` override.
if (tools) {
url.searchParams.set('tools', tools)
} else if (!profile) {
url.searchParams.set('tools', Object.keys(API_TOOL_SCHEMAS).join(','))
}

return url
Expand Down
134 changes: 60 additions & 74 deletions packages/api/src/tests/main.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { randomUUID } from 'node:crypto'
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
import { createMcpHandler, fromJsonSchema, type JsonSchemaType, McpServer } from '@modelcontextprotocol/server'
import type { YouSearchInput, YouSearchOutput } from '../main.ts'
import { createYouApi } from '../main.ts'

const testTools = [
type ToolSchema = {
properties?: Record<string, unknown>
required?: string[]
type: 'object'
}

const testTools: Array<{ inputSchema: ToolSchema; name: string; outputSchema?: ToolSchema }> = [
{
inputSchema: {
properties: {
Expand Down Expand Up @@ -65,14 +68,6 @@ const testTools = [
},
]

const transports = new Map<
string,
{
server: Server
transport: WebStandardStreamableHTTPServerTransport
}
>()

let originalFetch: typeof fetch
let tempDir: string
let traceFile: string
Expand Down Expand Up @@ -103,10 +98,6 @@ describe('createYouApi', () => {
process.env.YDC_ALLOWED_TOOLS = originalAllowedTools
}

await Promise.allSettled(
[...transports.values()].map(({ server, transport }) => Promise.all([server.close(), transport.close()])),
)
transports.clear()
rmSync(tempDir, { force: true, recursive: true })
})

Expand Down Expand Up @@ -174,7 +165,13 @@ describe('createYouApi', () => {

expect(tools.tools.map(({ name }) => name)).toEqual(['you-contents', 'you-research', 'you-search'])
const trace = readTrace()
expect(trace.every(({ url }) => url === 'https://api.you.com/mcp')).toBe(true)
expect(
trace.every(
({ url }) =>
url ===
'https://api.you.com/mcp?tools=you-answer%2Cyou-contents%2Cyou-discover%2Cyou-finance%2Cyou-research%2Cyou-search',
),
).toBe(true)
expect(trace.every(({ headers }) => headers.authorization === 'Bearer config-key')).toBe(true)
await you.close()
})
Expand All @@ -188,7 +185,13 @@ describe('createYouApi', () => {
await you.tools()

const trace = readTrace()
expect(trace.every(({ url }) => url === 'https://api.you.com/mcp')).toBe(true)
expect(
trace.every(
({ url }) =>
url ===
'https://api.you.com/mcp?tools=you-answer%2Cyou-contents%2Cyou-discover%2Cyou-finance%2Cyou-research%2Cyou-search',
),
).toBe(true)
await you.close()
})

Expand Down Expand Up @@ -249,7 +252,7 @@ describe('createYouApi', () => {
})

const createTestServer = () => {
const server = new Server(
const mcp = new McpServer(
{
name: 'test-api-mcp-server',
version: '1.0.0',
Expand All @@ -263,48 +266,55 @@ const createTestServer = () => {
},
)

server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: testTools,
}))
server.setRequestHandler(CallToolRequestSchema, async (request) => {
receivedToolInputs.push(request.params.arguments)

if (request.params.arguments?.query === 'missing-structured-content') {
return {
content: [],
}
}
for (const tool of testTools) {
mcp.registerTool(
tool.name,
{
inputSchema: fromJsonSchema(tool.inputSchema as JsonSchemaType),
},
async (args) => {
receivedToolInputs.push(args)
const query = (args as { query?: string } | undefined)?.query

if (query === 'missing-structured-content') {
return {
content: [],
}
}

if (request.params.arguments?.query === 'tool-error') {
return {
content: [],
isError: true,
structuredContent: {
message: 'Tool failed',
},
}
}
if (query === 'tool-error') {
return {
content: [],
isError: true,
structuredContent: {
message: 'Tool failed',
},
}
}

return {
content: [],
structuredContent: {
input: request.params.arguments,
ok: true,
return {
content: [],
structuredContent: {
input: args,
ok: true,
},
}
},
}
})
)
}

return server
return mcp
}

const handler = createMcpHandler(createTestServer, { legacy: 'stateless' })

const createMockedFetch = (activeTraceFile: string): typeof fetch =>
Object.assign(
async (input: string | URL | Request, init?: RequestInit | BunFetchRequestInit) => {
const request =
input instanceof Request
? new Request(input, init)
: new Request(input instanceof URL ? input.toString() : input, init)
const sessionId = request.headers.get('mcp-session-id')

await Bun.write(
activeTraceFile,
Expand All @@ -315,31 +325,7 @@ const createMockedFetch = (activeTraceFile: string): typeof fetch =>
})}\n`,
)

if (sessionId) {
const activeTransport = transports.get(sessionId)?.transport

if (activeTransport) {
return activeTransport.handleRequest(request)
}
}

const server = createTestServer()
let transport!: WebStandardStreamableHTTPServerTransport

transport = new WebStandardStreamableHTTPServerTransport({
enableJsonResponse: true,
onsessionclosed: (closedSessionId) => {
transports.delete(closedSessionId)
},
onsessioninitialized: (initializedSessionId) => {
transports.set(initializedSessionId, { server, transport })
},
sessionIdGenerator: () => randomUUID(),
})

await server.connect(transport)

return transport.handleRequest(request)
return handler.fetch(request)
},
{
preconnect: globalThis.fetch.preconnect.bind(globalThis.fetch),
Expand Down
Loading
Loading