@meetopenbot/notion 0.0.2 → 0.0.3

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 CHANGED
@@ -1,162 +1,19 @@
1
- import { definePlugin, shouldHandleInvoke, agentOutput, uiWidget, } from '@meetopenbot/plugin-sdk';
2
- import { generateText } from 'ai';
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
- configSchema: {
11
- type: 'object',
12
- properties: {
13
- notionApiKey: {
14
- type: 'string',
15
- description: 'Your Notion Internal Integration Token',
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.2",
3
+ "version": "0.0.3",
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
- "@ai-sdk/provider": "^4.0.7",
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.3.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
- }