OpenBI Docs Buy

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();
    }
}
  • ModelSummaryPlugin is the entry class plugin.json names. It needs a constructor without parameters; Initialize runs once, on the studio’s UI thread, when the plug-in is turned on.
  • AgentTools lists 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 for IStudioHost.
  • 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.
  • AgentInstructions is added to what the studio tells agents when they connect.
  • summary_top asks 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).

  1. In OpenBI Studio, under SPECIALTY in the navigation, click +. Specialty modules lists Model Summary.
  2. 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.
  3. 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

Last updated · OpenBI Studio 0.3.74