Skip to content

Commit 537ece7

Browse files
committed
Convert MCP Resource metadata to atomic resource #951
1 parent 17ad6bd commit 537ece7

File tree

4 files changed

+102
-28
lines changed

4 files changed

+102
-28
lines changed

browser/data-browser/src/components/AI/chatConversionUtils.ts

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,14 @@ import type {
1414
ToolResultPart,
1515
} from 'ai';
1616
import { newContextItem } from './AISidebarContext';
17-
import { type AIChatDisplayMessage, isMessageWithContext } from './types';
17+
import {
18+
type AIAtomicResourceMessageContext,
19+
type AIChatDisplayMessage,
20+
type AIMCPResourceMessageContext,
21+
type AIMessageContext,
22+
isAtomicResource,
23+
isMessageWithContext,
24+
} from './types';
1825

1926
// Not exported from 'ai' for some reason, for now we need to define it ourselves.
2027
type ReasoningPart = {
@@ -40,17 +47,14 @@ export const displayMessageToResource = async (
4047
message: AIChatDisplayMessage,
4148
parent: Resource<Ai.AiChat>,
4249
store: Store,
43-
context?: string[],
50+
context?: AIMessageContext[],
4451
): Promise<Resource<Ai.AiMessage>> => {
4552
if (isMessageWithContext(message)) {
46-
// TODO: Add context to the resource
47-
const contextSubjects = message.context.map(c => c.subject);
48-
4953
return displayMessageToResource(
5054
message.message,
5155
parent,
5256
store,
53-
contextSubjects,
57+
message.context,
5458
);
5559
}
5660

@@ -63,7 +67,11 @@ export const displayMessageToResource = async (
6367
});
6468

6569
if (context && context.length > 0) {
66-
messageResource.props.providedContext = context;
70+
const subjects = await Promise.all(
71+
context.map(c => contextToResource(c, messageResource, store)),
72+
);
73+
74+
messageResource.props.providedContext = subjects;
6775
}
6876

6977
if (typeof message.content === 'string') {
@@ -110,6 +118,33 @@ export const displayMessageToResource = async (
110118
return messageResource;
111119
};
112120

121+
const contextToResource = async (
122+
context: AIMessageContext,
123+
message: Resource<Ai.AiMessage>,
124+
store: Store,
125+
): Promise<string> => {
126+
if (isAtomicResource(context)) {
127+
return context.subject;
128+
}
129+
130+
const contextResource = await store.newResource<Ai.AiMessage>({
131+
isA: ai.classes.mcpResource,
132+
parent: message.subject,
133+
propVals: {
134+
[core.properties.name]: context.name,
135+
[ai.properties.mcpUri]: context.uri,
136+
[ai.properties.mcpServerId]: context.serverId,
137+
...(context.mimetype
138+
? { [server.properties.mimetype]: context.mimetype }
139+
: {}),
140+
},
141+
});
142+
143+
contextResource.save();
144+
145+
return contextResource.subject;
146+
};
147+
113148
export const messageResourcesToDisplayMessages = async (
114149
subjects: string[],
115150
store: Store,
@@ -126,6 +161,7 @@ export const messageResourcesToDisplayMessages = async (
126161
}
127162

128163
const role = tagToRole(resource.props.role);
164+
129165
const contentResources = await Promise.all(
130166
resource.props.content.map(s => store.getResource(s)),
131167
);
@@ -151,15 +187,20 @@ export const messageResourcesToDisplayMessages = async (
151187
};
152188

153189
if (resource.props.providedContext) {
190+
const context = (
191+
await Promise.allSettled(
192+
resource.props.providedContext.map(c =>
193+
resourceToAIMessageContext(c, store),
194+
),
195+
)
196+
)
197+
.filter(c => c.status === 'fulfilled')
198+
.map(c => c.value);
199+
154200
message = {
155201
role: 'annotated-message',
156202
message,
157-
context: resource.props.providedContext.map(c =>
158-
newContextItem({
159-
subject: c,
160-
type: 'resource',
161-
}),
162-
),
203+
context,
163204
};
164205
}
165206
}
@@ -240,6 +281,32 @@ export const messageResourcesToDisplayMessages = async (
240281
return messages;
241282
};
242283

284+
const resourceToAIMessageContext = async (
285+
subject: string,
286+
store: Store,
287+
): Promise<AIMessageContext> => {
288+
const resource = await store.getResource(subject);
289+
290+
if (resource.error) {
291+
throw resource.error;
292+
}
293+
294+
if (resource.hasClasses(ai.classes.mcpResource)) {
295+
return newContextItem<AIMCPResourceMessageContext>({
296+
type: 'mcp-resource',
297+
name: resource.props.name,
298+
uri: resource.props.mcpUri,
299+
serverId: resource.props.mcpServerId,
300+
mimetype: resource.props.mimetype,
301+
});
302+
}
303+
304+
return newContextItem<AIAtomicResourceMessageContext>({
305+
type: 'atomic-resource',
306+
subject: resource.subject,
307+
});
308+
};
309+
243310
const tagToRole = (subject: string) => {
244311
const tag = TAG_TO_ROLE_MAPPING[subject as keyof typeof TAG_TO_ROLE_MAPPING];
245312

browser/data-browser/src/components/AI/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export type AIMCPResourceMessageContext = {
3737
id: string;
3838
uri: string;
3939
name: string;
40+
mimetype?: string;
4041
serverId: string;
4142
};
4243

@@ -77,3 +78,15 @@ export function isMessageWithContext(
7778
): message is MessageWithContext {
7879
return message.role === 'annotated-message';
7980
}
81+
82+
export function isMCPResource(
83+
context: AIMessageContext,
84+
): context is AIMCPResourceMessageContext {
85+
return context.type === 'mcp-resource';
86+
}
87+
88+
export function isAtomicResource(
89+
context: AIMessageContext,
90+
): context is AIAtomicResourceMessageContext {
91+
return context.type === 'atomic-resource';
92+
}

browser/data-browser/src/components/AI/useProcessMessages.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ const processMCPResources = async (
145145
try {
146146
const resourceData = await readMCPResource(ctx.serverId, ctx.uri);
147147

148-
return `MCP resource "${ctx.name}" (${ctx.uri}):\n\`\`\`${resourceData.mimeType || 'text'}
148+
return `\`\`\`${resourceData.mimeType || 'text'}
149149
${typeof resourceData.contents === 'string' ? resourceData.contents : JSON.stringify(resourceData.contents, null, 2)}
150150
\`\`\``;
151151
} catch (error) {
@@ -195,7 +195,7 @@ const addContextToMessage = async (
195195

196196
// Add MCP context if we have any MCP resources
197197
if (mcpContent) {
198-
messageWithContext += `\n<extra-context>\n${mcpContent}\n</extra-context>`;
198+
messageWithContext += `\n<context>\n${mcpContent}\n</context>`;
199199
}
200200

201201
return messageWithContext;

browser/lib/src/ontologies/ai.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export const ai = {
2727
content:
2828
'https://atomicdata.dev/01jtjxtsa9syxmfca2zx5gcnmj/property/content',
2929
data: 'https://atomicdata.dev/01jtjxtsa9syxmfca2zx5gcnmj/property/data',
30+
mcpServerId:
31+
'https://atomicdata.dev/01jtjxtsa9syxmfca2zx5gcnmj/property/mcp-server-id',
3032
mcpUri:
3133
'https://atomicdata.dev/01jtjxtsa9syxmfca2zx5gcnmj/property/mcp-uri',
3234
messages:
@@ -35,10 +37,6 @@ export const ai = {
3537
'https://atomicdata.dev/01jtjxtsa9syxmfca2zx5gcnmj/property/provided-context',
3638
reasoningSignature:
3739
'https://atomicdata.dev/01jtjxtsa9syxmfca2zx5gcnmj/property/reasoning-signature',
38-
resourceBlob:
39-
'https://atomicdata.dev/01jtjxtsa9syxmfca2zx5gcnmj/property/resource-blob',
40-
resourceText:
41-
'https://atomicdata.dev/01jtjxtsa9syxmfca2zx5gcnmj/property/resource-text',
4240
role: 'https://atomicdata.dev/01jtjxtsa9syxmfca2zx5gcnmj/property/role',
4341
toolArguments:
4442
'https://atomicdata.dev/01jtjxtsa9syxmfca2zx5gcnmj/property/tool-arguments',
@@ -69,9 +67,8 @@ export const ai = {
6967
['https://atomicdata.dev/01jtjxtsa9syxmfca2zx5gcnmj/class/mcp-resource']: [
7068
'https://atomicdata.dev/properties/name',
7169
'https://atomicdata.dev/01jtjxtsa9syxmfca2zx5gcnmj/property/mcp-uri',
70+
'https://atomicdata.dev/01jtjxtsa9syxmfca2zx5gcnmj/property/mcp-server-id',
7271
'https://atomicdata.dev/properties/mimetype',
73-
'https://atomicdata.dev/01jtjxtsa9syxmfca2zx5gcnmj/property/resource-text',
74-
'https://atomicdata.dev/01jtjxtsa9syxmfca2zx5gcnmj/property/resource-blob',
7572
'https://atomicdata.dev/properties/description',
7673
],
7774
['https://atomicdata.dev/01jtjxtsa9syxmfca2zx5gcnmj/class/reasoning-part']:
@@ -133,11 +130,10 @@ declare module '../index.js' {
133130
requires:
134131
| BaseProps
135132
| 'https://atomicdata.dev/properties/name'
136-
| typeof ai.properties.mcpUri;
133+
| typeof ai.properties.mcpUri
134+
| typeof ai.properties.mcpServerId;
137135
recommends:
138136
| 'https://atomicdata.dev/properties/mimetype'
139-
| typeof ai.properties.resourceText
140-
| typeof ai.properties.resourceBlob
141137
| 'https://atomicdata.dev/properties/description';
142138
};
143139
[ai.classes.reasoningPart]: {
@@ -169,12 +165,11 @@ declare module '../index.js' {
169165
interface PropTypeMapping {
170166
[ai.properties.content]: string[];
171167
[ai.properties.data]: string;
168+
[ai.properties.mcpServerId]: string;
172169
[ai.properties.mcpUri]: string;
173170
[ai.properties.messages]: string[];
174171
[ai.properties.providedContext]: string[];
175172
[ai.properties.reasoningSignature]: string;
176-
[ai.properties.resourceBlob]: string;
177-
[ai.properties.resourceText]: string;
178173
[ai.properties.role]: string;
179174
[ai.properties.toolArguments]: string;
180175
[ai.properties.toolId]: string;
@@ -186,12 +181,11 @@ declare module '../index.js' {
186181
interface PropSubjectToNameMapping {
187182
[ai.properties.content]: 'content';
188183
[ai.properties.data]: 'data';
184+
[ai.properties.mcpServerId]: 'mcpServerId';
189185
[ai.properties.mcpUri]: 'mcpUri';
190186
[ai.properties.messages]: 'messages';
191187
[ai.properties.providedContext]: 'providedContext';
192188
[ai.properties.reasoningSignature]: 'reasoningSignature';
193-
[ai.properties.resourceBlob]: 'resourceBlob';
194-
[ai.properties.resourceText]: 'resourceText';
195189
[ai.properties.role]: 'role';
196190
[ai.properties.toolArguments]: 'toolArguments';
197191
[ai.properties.toolId]: 'toolId';

0 commit comments

Comments
 (0)