Skip to content

Commit fbcf13e

Browse files
authored
feat(search-mcp): add organization assistant and contextual retrieval (#7636)
1 parent 5465aed commit fbcf13e

43 files changed

Lines changed: 2760 additions & 1068 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/docs/content/docs/search/index.mdx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ For another client, choose **Other** and use the server URL with Streamable HTTP
9090

9191
Each person signs in with their own Sim account. MCP applies their current organization membership and document access; connecting an app does not add sources or grant new document permissions. To disconnect an app, open **Settings → General → Authorized apps** and revoke it.
9292

93+
MCP provides three tools for your organization:
94+
95+
- **search** finds indexed passages. Narrow results by source, modification date, or document.
96+
- **read_document** opens an indexed document by ID or its original URL. Read around a matching passage or page through longer documents. Results include a citation link.
97+
- **chat** asks the Sim Assistant for an answer with citations. Each call starts a new private conversation and can use the same search filters.
98+
99+
Search MCP is available in organization settings. All three tools use the caller’s current document permissions. They do not browse the web or change connected sources.
100+
93101
## Existing workspace Search
94102

95103
Workspace Search remains separate. Workspace admins add sources through **Search → Add source**; the member-account action is **Create & Invite**. Teammates need workspace access and connect from its source list. Organization Search does not automatically include workspace sources or grant access to workspace content.

‎apps/sim/app/.well-known/oauth-protected-resource/api/mcp/search/[workspaceId]/route.ts‎

Lines changed: 0 additions & 15 deletions
This file was deleted.

‎apps/sim/app/.well-known/oauth-protected-resource/api/mcp/search/organizations/[organizationId]/route.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ export const GET = withRouteHandler(
1010
if (isAuthDisabled) return new NextResponse(null, { status: 404 })
1111
const parsed = organizationKnowledgeMcpContract.params.safeParse(await context.params)
1212
if (!parsed.success) return new NextResponse(null, { status: 404 })
13-
return searchMcpResourceMetadata(getSearchMcpUrl('organization', parsed.data.organizationId))
13+
return searchMcpResourceMetadata(getSearchMcpUrl(parsed.data.organizationId))
1414
}
1515
)

‎apps/sim/app/api/auth/oauth2/authorize/route.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,8 @@ describe('OAuth2 authorize route', () => {
150150
{ scope: 'search:read' },
151151
{ scope: 'api:read', resource: `${BASE_URL}/api/mcp/search/organizations/org-1` },
152152
{ scope: 'search:read unknown', resource: `${BASE_URL}/api/mcp/search/organizations/org-1` },
153-
{ scope: 'search:read', resource: 'https://evil.example/api/mcp/search/org-1' },
153+
{ scope: 'search:read', resource: 'https://evil.example/api/mcp/search/organizations/org-1' },
154+
{ scope: 'search:read', resource: `${BASE_URL}/api/mcp/search/workspace-1` },
154155
])('refuses ambiguous or overly broad Search grants: %o', async (params) => {
155156
const response = await GET(
156157
request({

‎apps/sim/app/api/auth/oauth2/register/route.test.ts‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ beforeEach(() => {
3333
...(await req.clone().json()),
3434
client_id: 'client-1',
3535
client_id_issued_at: 1788000000,
36+
token_endpoint_auth_method: 'none',
3637
},
3738
{ status: 201 }
3839
)
@@ -97,9 +98,42 @@ describe('MCP public client registration', () => {
9798
})
9899
})
99100

101+
it.each(['client_secret_post', 'client_secret_basic'])(
102+
'lets the provider negotiate Claude-style %s registration to a public client',
103+
async (authMethod) => {
104+
const response = await POST(
105+
request({
106+
client_name: 'Claude',
107+
redirect_uris: ['https://claude.ai/api/mcp/auth_callback'],
108+
token_endpoint_auth_method: authMethod,
109+
scope: 'search:read offline_access',
110+
grant_types: ['authorization_code', 'refresh_token'],
111+
response_types: ['code'],
112+
application_type: 'web',
113+
client_secret: 'must-not-be-forwarded',
114+
})
115+
)
116+
expect(response.status).toBe(201)
117+
const body = await response.json()
118+
expect(body.token_endpoint_auth_method).toBe('none')
119+
expect(body).not.toHaveProperty('client_secret')
120+
const forwarded: Request = mocks.register.mock.calls[0][0]
121+
expect(await forwarded.json()).toEqual({
122+
client_name: 'Claude',
123+
redirect_uris: ['https://claude.ai/api/mcp/auth_callback'],
124+
token_endpoint_auth_method: authMethod,
125+
scope: 'search:read offline_access',
126+
grant_types: ['authorization_code', 'refresh_token'],
127+
response_types: ['code'],
128+
require_pkce: true,
129+
})
130+
}
131+
)
132+
100133
it.each([
101134
{ ...client, scope: 'api:write' },
102-
{ ...client, token_endpoint_auth_method: 'client_secret_post' },
135+
{ ...client, token_endpoint_auth_method: 'private_key_jwt' },
136+
{ ...client, token_endpoint_auth_method: 'unsupported' },
103137
{ ...client, grant_types: ['client_credentials'] },
104138
{ ...client, redirect_uris: ['http://evil.example/callback'] },
105139
{ ...client, redirect_uris: ['https://*.example/callback'] },

‎apps/sim/app/api/auth/oauth2/token/route.test.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,27 @@ describe('OAuth token route', () => {
335335
expect(response.status).toBe(200)
336336
})
337337

338+
it.each(['authorization_code', 'refresh_token'])(
339+
'rejects removed workspace resources before %s issuance',
340+
async (grantType) => {
341+
const response = await POST(
342+
tokenRequest(
343+
new URLSearchParams({
344+
grant_type: grantType,
345+
client_id: 'search-client',
346+
code: 'code',
347+
refresh_token: 'sim_ort_original',
348+
resource: 'https://sim.example/api/mcp/search/workspace-one',
349+
}).toString()
350+
)
351+
)
352+
expect(response.status).toBe(400)
353+
await expect(response.json()).resolves.toMatchObject({ error: 'invalid_target' })
354+
expect(mocks.betterAuthPost).not.toHaveBeenCalled()
355+
expect(mocks.rotate).not.toHaveBeenCalled()
356+
}
357+
)
358+
338359
it('passes a canonical resource to refresh rotation and preserves omission', async () => {
339360
const resource = 'https://sim.example/api/mcp/search/organizations/one'
340361
await POST(

‎apps/sim/app/api/mcp/search/[workspaceId]/route.ts‎

Lines changed: 0 additions & 9 deletions
This file was deleted.

‎apps/sim/app/api/mcp/search/organizations/[organizationId]/route.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createKnowledgeMcpHandlers } from '@/lib/knowledge/mcp/route-handler'
22

33
export const dynamic = 'force-dynamic'
44

5-
const handlers = createKnowledgeMcpHandlers('organization')
5+
const handlers = createKnowledgeMcpHandlers()
66

77
export const POST = handlers.POST
88
export const GET = handlers.GET
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
'use client'
22

3-
import { SearchMcpConnection } from '@/components/search-mcp-connection'
43
import { getSearchMcpUrl } from '@/lib/knowledge/mcp/urls'
54
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
5+
import { SearchMcpConnection } from '@/app/o/[organizationId]/settings/components/search-mcp-connection'
66

77
export function OrganizationSearchMcp() {
88
const { organization } = useOrganizationContext()
9-
const endpoint = getSearchMcpUrl('organization', organization.id)
9+
const endpoint = getSearchMcpUrl(organization.id)
1010

1111
return (
1212
<div className='flex max-w-xl flex-col gap-4'>
13-
<SearchMcpConnection key={organization.id} endpoint={endpoint} flush />
13+
<SearchMcpConnection key={organization.id} endpoint={endpoint} />
1414
</div>
1515
)
1616
}

apps/sim/components/search-mcp-connection.test.tsx renamed to apps/sim/app/o/[organizationId]/settings/components/search-mcp-connection.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { act } from 'react'
33
import { createRoot, type Root } from 'react-dom/client'
44
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5-
import { SearchMcpConnection } from '@/components/search-mcp-connection'
5+
import { SearchMcpConnection } from '@/app/o/[organizationId]/settings/components/search-mcp-connection'
66

77
const ENDPOINT = 'https://sim.fixture.test/api/mcp/search/organizations/org-1'
88

@@ -30,7 +30,7 @@ describe('Search MCP client connection', () => {
3030
})
3131

3232
async function render(endpoint = ENDPOINT) {
33-
await act(async () => root.render(<SearchMcpConnection endpoint={endpoint} flush />))
33+
await act(async () => root.render(<SearchMcpConnection endpoint={endpoint} />))
3434
}
3535

3636
async function selectClient(label: string) {
@@ -105,10 +105,10 @@ describe('Search MCP client connection', () => {
105105
})
106106

107107
it('quotes a shell metacharacter in the copied endpoint as a literal', async () => {
108-
await render("https://sim.fixture.test/api/mcp/search/workspace'$(example)")
108+
await render("https://sim.fixture.test/api/mcp/search/organizations/org'$(example)")
109109
await selectClient('Claude Code')
110110
expect(await copyConfiguration()).toBe(
111-
"claude mcp add --transport http sim-search 'https://sim.fixture.test/api/mcp/search/workspace'\\''$(example)'"
111+
"claude mcp add --transport http sim-search 'https://sim.fixture.test/api/mcp/search/organizations/org'\\''$(example)'"
112112
)
113113
})
114114

@@ -127,7 +127,7 @@ describe('Search MCP client connection', () => {
127127
await render()
128128
await selectClient(client)
129129
await copyConfiguration()
130-
const nextEndpoint = 'https://sim.fixture.test/api/mcp/search/workspace-2'
130+
const nextEndpoint = 'https://sim.fixture.test/api/mcp/search/organizations/org-2'
131131
await render(nextEndpoint)
132132
const value = await copyConfiguration()
133133
expect(value).toContain(nextEndpoint)

0 commit comments

Comments
 (0)