Skip to content
Closed
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
11 changes: 9 additions & 2 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ name: Run tests

on:
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, ready_for_review]

jobs:
test:
# Only run if PR author has write access or is a collaborator
if: github.event.pull_request.author_association == 'COLLABORATOR' || github.event.pull_request.author_association == 'OWNER' || github.event.pull_request.author_association == 'MEMBER'
if: >-
(github.event.pull_request.author_association == 'COLLABORATOR' ||
github.event.pull_request.author_association == 'OWNER' ||
github.event.pull_request.author_association == 'MEMBER') ||
github.actor == 'jules[bot]'
runs-on: ubuntu-latest

steps:
Expand All @@ -21,6 +25,9 @@ jobs:
- name: Install dependencies
run: npm install

- name: Build project
run: npm run build

- name: Create Google credentials file
shell: bash
run: |
Expand Down
10 changes: 10 additions & 0 deletions dataprompt.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { mcpPlugin } from './dist/plugins/mcp.js';

/** @type {import('./src/core/interfaces').DatapromptConfig} */
export default {
plugins: [
mcpPlugin({
url: 'http://localhost:3000/mcp', // Default MCP server URL
}),
],
};
56 changes: 56 additions & 0 deletions examples/mcp-weather/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { createPromptServer, McpTool, McpResource } from '../../src/index.js';

// Define a custom tool for getting weather data
const weatherTool: McpTool = {
// The 'get' function is what gets called from the prompt file
async get(params: { location: string }) {
console.log(`Fetching weather for ${params.location}...`);
// In a real implementation, this would call a weather API
return {
city: params.location,
temperature: 72,
forecast: 'Sunny',
};
}
};

// Define a custom resource for loading user data
const userResource: McpResource = {
async get(params: { id: string }) {
console.log(`Fetching user ${params.id}...`);
// In a real implementation, this would fetch from a database
return {
id: params.id,
name: 'Jane Doe',
email: '[email protected]',
};
}
};

async function start() {
// Create the server and get access to the store
const { server, store } = await createPromptServer({
// The configuration options must be passed inside a `config` object.
config: {
// Set the root directory to the current working directory.
rootDir: process.cwd(),
// Point to the prompts directory for this example
promptsDir: './examples/mcp-weather/prompts'
}
});

// Register the tool and resource. They are now first-class providers.
store.mcp.registerTool('weather', weatherTool);
store.mcp.registerResource('user', userResource);

console.log("Custom MCP providers 'weather' and 'user' are now registered.");

// Start the server
const port = 3000;
server.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
console.log('Try visiting http://localhost:3000/report?location=New%20York');
});
}

start();
28 changes: 28 additions & 0 deletions examples/mcp-weather/prompts/report.prompt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
model: googleai/gemini-2.5-flash-lite
data.prompt:
sources:
# 'weather' is a first-class data source provider.
# The key 'location' is passed as a parameter to the 'get' function of the weatherTool.
weather:
location: "{{request.query.location}}"

# 'user' is also a first-class provider.
# The key 'id' is passed to the 'get' function of the userResource.
user:
id: "user-123" # In a real app, this would be dynamic

output:
schema:
type: string
---

# Weather Report

Hi {{user.name}},

Here is the weather report for **{{weather.city}}**:
- **Temperature**: {{weather.temperature}} degrees
- **Forecast**: {{weather.forecast}}

Let me know if you need anything else!
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"@clack/prompts": "^0.10.0",
"@genkit-ai/flow": "^0.5.17",
"@genkit-ai/googleai": "^1.13.0",
"@modelcontextprotocol/sdk": "^1.20.0",
"date-fns": "^4.1.0",
"dotprompt": "^1.0.1",
"express": "^4.21.2",
Expand Down
25 changes: 25 additions & 0 deletions prompts/mcp-example.prompt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
model: googleai/gemini-1.5-flash-latest
data.prompt:
sources:
mcp:
config:
url: 'http://localhost:3001/mcp'
resources:
testResource:
uri: 'test://123'
tools:
testTool:
name: 'test-tool'
arguments:
input: 'test-input'
---

This is a test prompt that uses the MCP plugin with the official SDK.

Here is the data from the MCP sources:

Test Resource: {{json testResource}}
Test Tool: {{json testTool}}

Please confirm that the test was successful.
10 changes: 2 additions & 8 deletions src/core/dataprompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ import { TaskManager, createTaskManager } from '../routing/task-manager.js';
import { dateFormat } from '../utils/helpers/date-format.js';
import { findUp } from 'find-up';
import { pathToFileURL } from 'node:url';
import { DatapromptConfig, DatapromptUserConfig } from './config.js'
import { DatapromptConfig, DatapromptUserConfig } from './config.js';
import { ConfigManager } from './config.manager.js';

export interface DatapromptStore {
generate<Output = any>(url: string | Request | RequestContext): Promise<Output>;
registry: PluginManager;
Expand Down Expand Up @@ -93,15 +92,10 @@ export async function dataprompt(

return {
async generate<Output>(url: string | Request | RequestContext) {
// The generate method uses the RouteManager to find the route
// and then directly calls the universal helper to create the context.
const { route, request: reqFromManager } = await routeManager.getRequest(url);
if (!route) {
throw new Error(`No route found for ${typeof url === 'string' ? url : url.url}`);
}

// The RouteManager's getRequest handles context creation.
// We just need to call the flow with the context it provides.
return route.flow({ request: reqFromManager }) as Output;
},
routes: routeManager,
Expand All @@ -121,4 +115,4 @@ export async function createPromptServer(options: {
const store = await dataprompt(config);
const server = await createApiServer({ store, startTasks });
return { store, server };
}
}
10 changes: 3 additions & 7 deletions src/core/plugin.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { DatapromptPlugin, DataActionProvider, DataSourceProvider, TriggerProvid
import { firestorePlugin } from '../plugins/firebase/public.js';
import { schedulerPlugin } from '../plugins/scheduler/index.js';
import { fetchPlugin } from '../plugins/fetch/index.js';
import { mcpPlugin } from '../plugins/mcp.js';

export class PluginManager {
#dataSources = new Map<string, DataSourceProvider>();
#actions = new Map<string, DataActionProvider>();
#triggers = new Map<string, TriggerProvider>(); // Added map for triggers
#triggers = new Map<string, TriggerProvider>();

constructor(config: DatapromptConfig) {
const allPlugins = this.#resolvePlugins(config.plugins);
Expand All @@ -26,13 +27,12 @@ export class PluginManager {
const dataActionProvider = plugin.createDataAction();
this.#actions.set(dataActionProvider.name, dataActionProvider);
}

if (plugin.createTrigger) {
const triggerProvider = plugin.createTrigger();
this.#triggers.set(triggerProvider.name, triggerProvider);
}
}

#resolvePlugins = (userPlugins: DatapromptPlugin[] = []): DatapromptPlugin[] => {
const plugins = [...userPlugins];
if (!plugins.some(p => p.name === 'firestore')) plugins.push(firestorePlugin());
Expand All @@ -57,10 +57,6 @@ export class PluginManager {
return provider;
}

/**
* NEW: Retrieves a trigger provider by its registered name.
* @param name The name of the trigger provider (e.g., 'schedule').
*/
public getTrigger(name: string): TriggerProvider {
const provider = this.#triggers.get(name);
if (!provider) {
Expand Down
46 changes: 24 additions & 22 deletions src/core/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@ export function createGenkitPrompt(options: {
const { ai, flowDef } = options;
const { data, name, promptMetadata, outputSchema, template } = flowDef;
const sources = data?.prompt?.sources || {};
// The input schema should have a key for each data source provider.
const promptInputSchema = z.object({
...Object.fromEntries(
Object.entries(sources).flatMap(([sourceName, sourceConfig]) => {
return Object.keys(sourceConfig).map(propertyName => [propertyName, z.any()])
})
Object.keys(sources).map(sourceName => [sourceName, z.any()])
),
request: RequestContextSchema,
});
Expand Down Expand Up @@ -82,48 +81,51 @@ export class Prompt {

/**
* Fetches data from all sources defined in the prompt's frontmatter.
* This version is refactored to support the flat API structure.
*/
async #fetchData(request: RequestContext): Promise<Record<string, any>> {
const sources = this.#flowDef.data?.prompt?.sources || {};
const promptSources: Record<string, any> = {};

for (const [sourceName, sourceConfig] of Object.entries(sources)) {
const sourceProvider = this.#pluginManager.getDataSource(sourceName);
for (const [propertyName, config] of Object.entries(sourceConfig)) {
const data = await this.#ai.run(`DataSource: ${sourceName}.${propertyName}`, async () => {
const processedConfig = processTemplates(handlebars, config, { request });
return await sourceProvider.fetchData({
request,
config: processedConfig,
file: this.#file,
});
for (const [providerName, providerConfig] of Object.entries(sources)) {
const sourceProvider = this.#pluginManager.getDataSource(providerName);
const data = await this.#ai.run(`DataSource: ${providerName}`, async () => {
// The entire config for the provider is processed for templates.
const processedConfig = processTemplates(handlebars, providerConfig, { request });
return await sourceProvider.fetchData({
request,
config: processedConfig,
file: this.#file,
});
promptSources[propertyName] = data;
}
});
// The result is stored under the provider's name (e.g., 'weather').
promptSources[providerName] = data;
}
return promptSources;
}

/**
* Executes all result actions defined in the prompt's frontmatter.
* This version is refactored to support the flat API structure.
*/
async #executeActions(request: RequestContext, promptSources: Record<string, any>, result: { output: any }): Promise<void> {
const resultActions = this.#flowDef.data?.prompt?.result || {};
for (const [actionName, actionConfig] of Object.entries(resultActions)) {
await this.#ai.run(`ResultAction: ${actionName}`, async () => {
const actionProvider = this.#pluginManager.getAction(actionName);
for (const [providerName, providerConfig] of Object.entries(resultActions)) {
await this.#ai.run(`ResultAction: ${providerName}`, async () => {
const actionProvider = this.#pluginManager.getAction(providerName);
const templateData = { ...promptSources, request, output: result.output };
const processedConfig = processTemplates(
handlebars,
actionConfig,
{ ...promptSources, request, output: result.output }
providerConfig,
templateData
);
await actionProvider.execute({
request,
config: processedConfig,
promptSources: { ...promptSources, output: result.output },
promptSources: templateData,
file: this.#file,
});
});
}
}
}
}
19 changes: 11 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
export { DatapromptRoute } from './routing/server.js';
export { DatapromptConfig } from './core/config.js'
export { dataprompt, createPromptServer } from './core/dataprompt.js'
export {
RequestContextSchema,
RequestContext,
DatapromptPlugin,
// Export runtime values
export { dataprompt, createPromptServer } from './core/dataprompt.js';
export { RequestContextSchema } from './core/interfaces.js';

// Export type-only interfaces and types
export type { DatapromptRoute } from './routing/server.js';
export type { DatapromptConfig, DatapromptUserConfig } from './core/config.js';
export type {
RequestContext,
DatapromptPlugin,
DataSourceProvider,
DataActionProvider,
TriggerConfig,
TriggerProvider,
} from './core/interfaces.js'
} from './core/interfaces.js';
2 changes: 1 addition & 1 deletion src/plugins/firebase/public.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { getFirebaseApp } from './app.js'
export { firestorePlugin } from './firestore/index.js'
export { FirebasePluginConfig } from './types.js'
export type { FirebasePluginConfig } from './types.js'
Loading
Loading