@meetopenbot/notion 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +15 -158
- package/package.json +2 -10
- package/dist/conversation-history.js +0 -164
package/dist/index.js
CHANGED
|
@@ -1,162 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { createOpenAI } from '@ai-sdk/openai';
|
|
4
|
-
import { createMCPClient } from '@ai-sdk/mcp';
|
|
5
|
-
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
6
|
-
import { buildConversationMessages, trimHistory, MAX_HISTORY_MESSAGES, } from './conversation-history.js';
|
|
7
|
-
export default definePlugin({
|
|
1
|
+
import { defineMcpAgent } from '@meetopenbot/plugin-sdk';
|
|
2
|
+
export const plugin = await defineMcpAgent({
|
|
8
3
|
name: 'Notion Agent',
|
|
9
4
|
description: 'An agent that can interact with your Notion workspace using the official MCP server.',
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
format: 'password'
|
|
17
|
-
},
|
|
18
|
-
openaiApiKey: {
|
|
19
|
-
type: 'string',
|
|
20
|
-
description: 'Your OpenAI API Key',
|
|
21
|
-
format: 'password'
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
required: [],
|
|
25
|
-
},
|
|
26
|
-
factory: (context) => (builder) => {
|
|
27
|
-
const openai = createOpenAI({
|
|
28
|
-
apiKey: context.config.openaiApiKey || process.env.OPENAI_API_KEY,
|
|
29
|
-
});
|
|
30
|
-
builder.on('client:ui:widget:response', async function* (event) {
|
|
31
|
-
if (event.data.widgetId === 'notion-auth-form') {
|
|
32
|
-
const apiKey = event.data.values?.notionApiKey;
|
|
33
|
-
if (apiKey) {
|
|
34
|
-
await context.storage.createVariable({
|
|
35
|
-
key: 'NOTION_TOKEN',
|
|
36
|
-
value: apiKey,
|
|
37
|
-
secret: true
|
|
38
|
-
});
|
|
39
|
-
yield agentOutput({
|
|
40
|
-
agentId: context.agentId,
|
|
41
|
-
content: '✅ Notion API key saved! You can now use the Notion agent. Please try your request again.',
|
|
42
|
-
threadId: event.meta?.threadId
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
builder.on('agent:invoke', async function* (event, ctx) {
|
|
48
|
-
if (!shouldHandleInvoke(event, context.agentId))
|
|
49
|
-
return;
|
|
50
|
-
const threadId = event.meta?.threadId ?? ctx.state.threadId;
|
|
51
|
-
const userMessage = event.data.content;
|
|
52
|
-
const channelId = ctx.state.channelId;
|
|
53
|
-
// Check for Notion token in config or storage
|
|
54
|
-
const variables = await context.storage.getVariables();
|
|
55
|
-
const storedToken = variables['NOTION_TOKEN'];
|
|
56
|
-
const notionToken = context.config.notionApiKey ||
|
|
57
|
-
(typeof storedToken === 'string' ? storedToken : storedToken?.value);
|
|
58
|
-
if (!notionToken) {
|
|
59
|
-
yield agentOutput({
|
|
60
|
-
agentId: context.agentId,
|
|
61
|
-
content: 'I need your Notion API key to proceed. Please provide it below:',
|
|
62
|
-
threadId
|
|
63
|
-
});
|
|
64
|
-
yield uiWidget({
|
|
65
|
-
agentId: context.agentId,
|
|
66
|
-
widget: {
|
|
67
|
-
kind: 'form',
|
|
68
|
-
widgetId: 'notion-auth-form',
|
|
69
|
-
title: 'Notion Authentication',
|
|
70
|
-
fields: [
|
|
71
|
-
{
|
|
72
|
-
id: 'notionApiKey',
|
|
73
|
-
label: 'Notion API Key',
|
|
74
|
-
type: 'text',
|
|
75
|
-
placeholder: 'secret_...',
|
|
76
|
-
required: true
|
|
77
|
-
}
|
|
78
|
-
],
|
|
79
|
-
submitLabel: 'Save Key'
|
|
80
|
-
},
|
|
81
|
-
threadId
|
|
82
|
-
});
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
let conversationMessages = [{ role: 'user', content: userMessage }];
|
|
86
|
-
if (channelId) {
|
|
87
|
-
try {
|
|
88
|
-
const rawEvents = await context.storage.getEvents({
|
|
89
|
-
channelId,
|
|
90
|
-
...(threadId ? { threadId } : {}),
|
|
91
|
-
});
|
|
92
|
-
conversationMessages = trimHistory(buildConversationMessages(rawEvents, context.agentId, userMessage), MAX_HISTORY_MESSAGES);
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
// Fall back to single-turn if history cannot be loaded
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
// Initialize the Notion MCP client using stdio transport
|
|
99
|
-
const mcpClient = await createMCPClient({
|
|
100
|
-
transport: new StdioClientTransport({
|
|
101
|
-
command: 'npx',
|
|
102
|
-
args: ['-y', '@notionhq/notion-mcp-server'],
|
|
103
|
-
env: {
|
|
104
|
-
NOTION_TOKEN: notionToken,
|
|
105
|
-
// Pass through path and other essential env vars
|
|
106
|
-
PATH: process.env.PATH || '',
|
|
107
|
-
HOME: process.env.HOME || '',
|
|
108
|
-
},
|
|
109
|
-
}),
|
|
110
|
-
});
|
|
111
|
-
try {
|
|
112
|
-
// Discover all tools from the Notion MCP server
|
|
113
|
-
const tools = await mcpClient.tools();
|
|
114
|
-
const result = await generateText({
|
|
115
|
-
model: openai('gpt-4o'),
|
|
116
|
-
system: `You are a helpful Notion assistant. You have access to the official Notion MCP tools.
|
|
117
|
-
Use these tools to search, read, create, and update content in the user's Notion workspace.
|
|
118
|
-
Always explain what you are doing. If you need a page or database ID, use the search tools first.
|
|
119
|
-
You may see prior user and assistant turns in the conversation; use them for continuity and follow-ups.`,
|
|
120
|
-
messages: conversationMessages,
|
|
121
|
-
// @ts-ignore
|
|
122
|
-
maxSteps: 10,
|
|
123
|
-
tools,
|
|
124
|
-
});
|
|
125
|
-
// Log tool usage to the UI using widgets
|
|
126
|
-
for (const step of result.steps) {
|
|
127
|
-
if (step.toolResults) {
|
|
128
|
-
for (const toolResult of step.toolResults) {
|
|
129
|
-
yield uiWidget({
|
|
130
|
-
agentId: context.agentId,
|
|
131
|
-
widget: {
|
|
132
|
-
kind: 'message',
|
|
133
|
-
title: `Notion Result: ${toolResult.toolName}`,
|
|
134
|
-
// @ts-ignore
|
|
135
|
-
body: `Input: ${JSON.stringify(toolResult.input, null, 2)}\nOutput: ${JSON.stringify(toolResult.result || toolResult.output, null, 2)}`,
|
|
136
|
-
display: 'collapsed'
|
|
137
|
-
},
|
|
138
|
-
threadId
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
yield agentOutput({
|
|
144
|
-
agentId: context.agentId,
|
|
145
|
-
content: result.text,
|
|
146
|
-
threadId,
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
yield agentOutput({
|
|
151
|
-
agentId: context.agentId,
|
|
152
|
-
content: `I encountered an error while communicating with Notion: ${error.message}`,
|
|
153
|
-
threadId,
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
finally {
|
|
157
|
-
// Ensure the MCP connection is closed to prevent resource leaks
|
|
158
|
-
await mcpClient.close();
|
|
159
|
-
}
|
|
160
|
-
});
|
|
5
|
+
models: { providers: ['openai'], default: 'openai/gpt-4o-mini' },
|
|
6
|
+
secret: { envKeys: ['NOTION_TOKEN'] },
|
|
7
|
+
mcp: {
|
|
8
|
+
command: 'npx',
|
|
9
|
+
args: ['-y', '@notionhq/notion-mcp-server'],
|
|
10
|
+
env: (token) => ({ NOTION_TOKEN: token }),
|
|
161
11
|
},
|
|
12
|
+
system: `You are a helpful Notion assistant. You have access to the official Notion MCP tools.
|
|
13
|
+
Use these tools to search, read, create, and update content in the user's Notion workspace.
|
|
14
|
+
If you need a page or database ID, use the search tools first.
|
|
15
|
+
You may see prior user and assistant turns in the conversation; use them for continuity and follow-ups.`,
|
|
16
|
+
maxSteps: 10,
|
|
17
|
+
history: true,
|
|
162
18
|
});
|
|
19
|
+
export default plugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meetopenbot/notion",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Notion agent kind plugin for OpenBot",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -16,15 +16,7 @@
|
|
|
16
16
|
"author": "",
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@
|
|
20
|
-
"@ai-sdk/provider-utils": "^5.0.27",
|
|
21
|
-
"@notionhq/client": "latest",
|
|
22
|
-
"ai": "latest",
|
|
23
|
-
"@ai-sdk/openai": "latest",
|
|
24
|
-
"@ai-sdk/mcp": "latest",
|
|
25
|
-
"@modelcontextprotocol/sdk": "latest",
|
|
26
|
-
"zod": "latest",
|
|
27
|
-
"@meetopenbot/plugin-sdk": "^0.2.0"
|
|
19
|
+
"@meetopenbot/plugin-sdk": "^0.4.0"
|
|
28
20
|
},
|
|
29
21
|
"devDependencies": {
|
|
30
22
|
"typescript": "^5.0.0",
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
/** Cap on model messages (user, assistant, tool); includes tool-call/result pairs. */
|
|
2
|
-
export const MAX_HISTORY_MESSAGES = 60;
|
|
3
|
-
let toolSequence = 0;
|
|
4
|
-
function nextToolCallId() {
|
|
5
|
-
return `hist-tool-${toolSequence++}`;
|
|
6
|
-
}
|
|
7
|
-
function resetToolIds() {
|
|
8
|
-
toolSequence = 0;
|
|
9
|
-
}
|
|
10
|
-
function tryParseJson(raw) {
|
|
11
|
-
const s = raw.trim();
|
|
12
|
-
if (!s)
|
|
13
|
-
return '';
|
|
14
|
-
try {
|
|
15
|
-
return JSON.parse(s);
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return s;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
/** Parses `Input: ...` / `Output: ...` blocks from persisted UI widget bodies. */
|
|
22
|
-
function parseNotionToolBody(body) {
|
|
23
|
-
const t = body.trim();
|
|
24
|
-
const outMarker = '\nOutput:';
|
|
25
|
-
const outIdx = t.indexOf(outMarker);
|
|
26
|
-
if (outIdx === -1) {
|
|
27
|
-
const onlyOut = t.match(/^Output:\s*([\s\S]*)$/i);
|
|
28
|
-
if (onlyOut)
|
|
29
|
-
return { output: tryParseJson(onlyOut[1]) };
|
|
30
|
-
const onlyIn = t.match(/^Input:\s*([\s\S]*)$/i);
|
|
31
|
-
if (onlyIn)
|
|
32
|
-
return { input: tryParseJson(onlyIn[1]) };
|
|
33
|
-
return {};
|
|
34
|
-
}
|
|
35
|
-
const head = t.slice(0, outIdx);
|
|
36
|
-
const tail = t.slice(outIdx + outMarker.length);
|
|
37
|
-
const inMatch = head.match(/^Input:\s*([\s\S]*)$/i);
|
|
38
|
-
const input = inMatch ? tryParseJson(inMatch[1]) : undefined;
|
|
39
|
-
const output = tryParseJson(tail);
|
|
40
|
-
return { input, output };
|
|
41
|
-
}
|
|
42
|
-
function toolOutputFromUnknown(value) {
|
|
43
|
-
if (value === undefined || value === null) {
|
|
44
|
-
return { type: 'text', value: '' };
|
|
45
|
-
}
|
|
46
|
-
if (typeof value === 'string') {
|
|
47
|
-
return { type: 'text', value };
|
|
48
|
-
}
|
|
49
|
-
return { type: 'json', value: value };
|
|
50
|
-
}
|
|
51
|
-
const ACTION_TITLE = /^Notion Action:\s*(.+)$/i;
|
|
52
|
-
const RESULT_TITLE = /^Notion Result:\s*(.+)$/i;
|
|
53
|
-
function pushToolRound(messages, toolName, input, output) {
|
|
54
|
-
const toolCallId = nextToolCallId();
|
|
55
|
-
messages.push({
|
|
56
|
-
role: 'assistant',
|
|
57
|
-
content: [
|
|
58
|
-
{
|
|
59
|
-
type: 'tool-call',
|
|
60
|
-
toolCallId,
|
|
61
|
-
toolName,
|
|
62
|
-
input,
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
});
|
|
66
|
-
messages.push({
|
|
67
|
-
role: 'tool',
|
|
68
|
-
content: [
|
|
69
|
-
{
|
|
70
|
-
type: 'tool-result',
|
|
71
|
-
toolCallId,
|
|
72
|
-
toolName,
|
|
73
|
-
output: toolOutputFromUnknown(output),
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Walks persisted `events` in order and builds model messages: user turns, tool call/result
|
|
80
|
-
* pairs from `client:ui:widget` (Notion Action / Notion Result), and assistant text from
|
|
81
|
-
* `agent:output`.
|
|
82
|
-
*/
|
|
83
|
-
export function buildConversationMessages(events, agentId, currentUserContent) {
|
|
84
|
-
const list = [];
|
|
85
|
-
const raw = Array.isArray(events) ? events : [];
|
|
86
|
-
let pendingToolName;
|
|
87
|
-
let pendingInput;
|
|
88
|
-
resetToolIds();
|
|
89
|
-
for (const item of raw) {
|
|
90
|
-
const e = item;
|
|
91
|
-
if (e.type === 'agent:invoke') {
|
|
92
|
-
const data = e.data;
|
|
93
|
-
const role = data?.role ?? 'user';
|
|
94
|
-
const text = data?.content;
|
|
95
|
-
if (role === 'user' && typeof text === 'string' && text.length > 0) {
|
|
96
|
-
resetToolIds();
|
|
97
|
-
list.push({ role: 'user', content: text });
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
else if (e.type === 'client:ui:widget' && e.meta?.agentId === agentId) {
|
|
101
|
-
const d = e.data;
|
|
102
|
-
if (d.kind !== 'message' || typeof d.body !== 'string' || !d.title)
|
|
103
|
-
continue;
|
|
104
|
-
const actionMatch = d.title.match(ACTION_TITLE);
|
|
105
|
-
if (actionMatch) {
|
|
106
|
-
const { input } = parseNotionToolBody(d.body);
|
|
107
|
-
pendingToolName = actionMatch[1].trim();
|
|
108
|
-
pendingInput = input;
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
const resultMatch = d.title.match(RESULT_TITLE);
|
|
112
|
-
if (!resultMatch)
|
|
113
|
-
continue;
|
|
114
|
-
const toolName = resultMatch[1].trim();
|
|
115
|
-
const { input: bodyInput, output } = parseNotionToolBody(d.body);
|
|
116
|
-
const input = bodyInput !== undefined
|
|
117
|
-
? bodyInput
|
|
118
|
-
: pendingToolName === toolName
|
|
119
|
-
? pendingInput
|
|
120
|
-
: undefined;
|
|
121
|
-
if (output !== undefined) {
|
|
122
|
-
pushToolRound(list, toolName, input ?? {}, output);
|
|
123
|
-
}
|
|
124
|
-
pendingToolName = undefined;
|
|
125
|
-
pendingInput = undefined;
|
|
126
|
-
}
|
|
127
|
-
else if (e.type === 'agent:output' && e.meta?.agentId === agentId) {
|
|
128
|
-
const text = e.data?.content;
|
|
129
|
-
if (typeof text === 'string' && text.trim()) {
|
|
130
|
-
list.push({ role: 'assistant', content: text.trim() });
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return finalizeCurrentUserTurn(list, currentUserContent);
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Ensures the pending user message is included exactly once (matches storage timing).
|
|
138
|
-
*/
|
|
139
|
-
function userTextContent(msg) {
|
|
140
|
-
if (msg.role !== 'user')
|
|
141
|
-
return undefined;
|
|
142
|
-
const c = msg.content;
|
|
143
|
-
return typeof c === 'string' ? c : undefined;
|
|
144
|
-
}
|
|
145
|
-
function finalizeCurrentUserTurn(messages, currentUserContent) {
|
|
146
|
-
const last = messages[messages.length - 1];
|
|
147
|
-
if (!last) {
|
|
148
|
-
return [{ role: 'user', content: currentUserContent }];
|
|
149
|
-
}
|
|
150
|
-
if (last.role === 'assistant' || last.role === 'tool') {
|
|
151
|
-
return [...messages, { role: 'user', content: currentUserContent }];
|
|
152
|
-
}
|
|
153
|
-
if (last.role === 'user') {
|
|
154
|
-
if (userTextContent(last) === currentUserContent)
|
|
155
|
-
return messages;
|
|
156
|
-
return [...messages, { role: 'user', content: currentUserContent }];
|
|
157
|
-
}
|
|
158
|
-
return messages;
|
|
159
|
-
}
|
|
160
|
-
export function trimHistory(messages, max) {
|
|
161
|
-
if (messages.length <= max)
|
|
162
|
-
return messages;
|
|
163
|
-
return messages.slice(messages.length - max);
|
|
164
|
-
}
|