Your first plug-in
Model Summary is a plug-in with two agent tools and no tab: one counts the open model’s measures by display folder, the other ranks a column’s values by one of the model’s measures. Three files, one build, and your agent has two new tools.
The folder
Unzip the SDK beside your plug-in’s folder, and give them a nuget.config that names the SDK’s packages (see Set up):
nuget.config
OpenBI-SDK-0.3.74/ (the SDK's zip, unzipped)
ModelSummary/
ModelSummary.csproj
plugin.json
ModelSummaryPlugin.cs
<?xml version="1.0" encoding="utf-8"?>
<!-- The OpenBI SDK's packages folder as a package source, beside nuget.org: unzip OpenBI-SDK-0.3.74.zip (www.openbi.no/sdk/)
into this folder, so that OpenBI-SDK-0.3.74/packages is beside ModelSummary/, then `dotnet build -c Release` in
ModelSummary/. -->
<configuration>
<packageSources>
<add key="openbi-sdk" value="OpenBI-SDK-0.3.74/packages" />
</packageSources>
</configuration>
The project
<Project Sdk="Microsoft.NET.Sdk">
<!-- Model Summary: the docs' sample plug-in (docs/sdk/first-plugin/). It builds on Windows, macOS and Linux with
`dotnet build`. OpenBI.Studio.Sdk comes from the OpenBI SDK's packages folder (nuget.config beside this folder) and
ModelContextProtocol from nuget.org. Neither is copied to the output: inside the studio, the studio's own run. -->
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>ModelSummary</RootNamespace>
<Version>1.0.0</Version>
<!-- a plug-in is loaded into the studio at run time: its own dependencies (none here) go beside it -->
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
<ItemGroup>
<!-- PrivateAssets: a test project that references this one compiles against the studio's own assemblies instead -->
<PackageReference Include="OpenBI.Studio.Sdk" Version="0.3.74" ExcludeAssets="runtime" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<None Update="plugin.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
The manifest
id, assembly and entry are required. Use an id with a prefix of your own: the studio remembers by it whether the plug-in is on, and keeps its settings under it.
{
"id": "sample.model-summary",
"name": "Model Summary",
"version": "1.0.0",
"publisher": "OpenBI docs sample",
"description": "A sample plug-in: it gives the studio's agent two tools, summary_folders (the model's measures by display folder) and summary_top (a column's values ranked by a measure).",
"assembly": "ModelSummary.dll",
"entry": "ModelSummary.ModelSummaryPlugin",
"minStudio": "0.3.74"
}
The code
using System.ComponentModel;
using System.Text;
using ModelContextProtocol.Server;
using OpenBI.Studio.Core.Engine;
using OpenBI.Studio.Sdk;
namespace ModelSummary;
/// <summary>
/// Model Summary: a small OpenBI Studio plug-in with two agent tools and no tab. The studio makes it (a constructor
/// without parameters) when it is turned on under Specialty modules, and calls Initialize with the host.
/// </summary>
public sealed class ModelSummaryPlugin : IStudioPlugin
{
/// <summary>plugin.json's id.</summary>
public const string Id = "sample.model-summary";
public void Initialize(IStudioHost host) =>
host.Log("Model Summary is on", $"A sample plug-in of the OpenBI SDK, in OpenBI Studio {host.StudioVersion}");
/// <summary>No tab under Specialty: its tools are its whole work.</summary>
public IReadOnlyList<PluginView> Views => [];
/// <summary>Its agent tools: they join the studio's own while the plug-in is on.</summary>
public IReadOnlyList<Type> AgentTools => [typeof(SummaryTools)];
/// <summary>What agents are told about the tools (added to the studio's instructions).</summary>
public string? AgentInstructions =>
"Model Summary (a sample plug-in): summary_folders counts the open model's measures by display folder; " +
"summary_top ranks a column's values by one of the model's measures.";
}
/// <summary>The plug-in's agent tools. The studio makes the class with its services, IStudioHost among them.</summary>
[McpServerToolType]
public sealed class SummaryTools(IStudioHost host)
{
[McpServerTool(Name = "summary_folders", ReadOnly = true)]
[Description("Counts the measures of the model open in OpenBI Studio by display folder (measures in no folder under \"(none)\").")]
public string Folders()
{
if (!host.HasProject) return "No project is open in OpenBI Studio.";
var folders = host.Tables
.SelectMany(t => t.MeasureDetails)
.GroupBy(m => string.IsNullOrEmpty(m.Folder) ? "(none)" : m.Folder)
.OrderBy(g => g.Key, StringComparer.OrdinalIgnoreCase);
var text = new StringBuilder($"Measures by display folder in {host.ProjectName}:\n");
foreach (var folder in folders) text.Append($"- {folder.Key}: {folder.Count()}\n");
return text.ToString();
}
[McpServerTool(Name = "summary_top", ReadOnly = true)]
[Description("Ranks a column's values by one of the model's measures, highest first, with a DAX query on the model open in OpenBI Studio.")]
public async Task<string> Top(
[Description("The column's table")] string table,
[Description("The column")] string column,
[Description("The measure's name")] string measure,
[Description("How many values (10 unless said, at most 100)")] int top = 10,
CancellationToken ct = default)
{
if (!host.HasProject) return "No project is open in OpenBI Studio.";
var t = table.Replace("'", "''");
var c = column.Replace("]", "]]");
var m = measure.Replace("]", "]]");
var dax = $"EVALUATE TOPN ( {Math.Clamp(top, 1, 100)}, SUMMARIZECOLUMNS ( '{t}'[{c}], \"Value\", [{m}] ), [Value], DESC ) " +
"ORDER BY [Value] DESC";
QueryResult result = await host.DaxAsync(dax, ct);
host.Log($"Model Summary ranked {column}", $"by [{measure}]", byAgent: true);
var text = new StringBuilder($"{column} by [{measure}], highest first:\n");
foreach (var row in result.Rows) text.Append($"- {QueryResult.Text(row[0])}: {QueryResult.Text(row[1])}\n");
return text.ToString();
}
}
ModelSummaryPluginis the entry classplugin.jsonnames. It needs a constructor without parameters;Initializeruns once, on the studio’s UI thread, when the plug-in is turned on.AgentToolslists the tool classes. Each is a ModelContextProtocol[McpServerToolType]class whose[McpServerTool]methods are the tools; the studio makes it with its services, so the constructor asks forIStudioHost.- The tool names share one list with the studio’s ~150 tools: give them a prefix of your own (
summary_). Their descriptions are the agent’s manual: say what the tool does and what it returns. AgentInstructionsis added to what the studio tells agents when they connect.summary_topasks the model’s own measure for the number, with an OAX query, and writes a line in the Activity panel as the agent’s.
Build it
cd ModelSummary
dotnet build -c Release
The plug-in is the output folder, ModelSummary/bin/Release/net10.0/: ModelSummary.dll, ModelSummary.deps.json and plugin.json (a .pdb and a .runtimeconfig.json too, which do no harm). The SDK’s assemblies are not copied there.
Try it in the studio
Copy the output into your own Plugins folder:
$dest = "$env:LOCALAPPDATA\OpenBI\Plugins\model-summary"
New-Item -ItemType Directory -Force $dest | Out-Null
Copy-Item bin\Release\net10.0\* $dest
dest="$HOME/Library/Application Support/OpenBI/Plugins/model-summary"
mkdir -p "$dest" && cp bin/Release/net10.0/* "$dest/"
The installed OpenBI Studio runs only signed plug-ins, on Windows and on a Mac: unsigned, Model Summary is listed with “it is not signed”. On a test computer, an administrator can allow unsigned plug-ins (on a Mac, AllowUnsignedPlugins in a configuration profile: see Your organization’s policy):
reg add HKLM\SOFTWARE\Policies\NorwayAnalytics\OpenBI /v AllowUnsignedPlugins /t REG_DWORD /d 1 /f
To ship it, sign it instead (see Signing).
- In OpenBI Studio, under SPECIALTY in the navigation, click +. Specialty modules lists Model Summary.
- Turn it on and close the window. The Activity panel says “Model Summary is on”, and the agent endpoint starts again with the new tools.
- Ask your agent, connected to the studio:
Use summary_folders: how are the measures of my model organised?
Change it and build again
A plug-in that was turned on stays loaded until the studio closes: turning it off removes its tools and tabs, not its assemblies, and on Windows its files stay in use. To try a new build, close the studio, copy the new files and start it again. Or zip the output folder and install it with Install a plug-in…: the new version waits beside the old one and replaces it when the studio starts again.
Where next
- What a plug-in can do: tabs, settings and secrets, sign-ins, privacy.
- Testing: Model Summary’s tools tested without the studio.
- Packaging and distribution: the zip, signing, installing.