Add configurable MCP workspace guidance (#11839)

* Add configurable MCP workspace guidance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix Instructions passing, tweak UI

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Moor
2026-03-22 10:45:09 -04:00
committed by GitHub
parent a4badbea9c
commit fddf630e49
9 changed files with 81 additions and 2 deletions
+4
View File
@@ -64,6 +64,10 @@ class Team extends Model {
@observable
defaultUserRole: UserRole;
@Field
@observable
guidanceMCP: string | null;
@Field
@observable
preferences: TeamPreferences | null;
+39
View File
@@ -4,6 +4,7 @@ import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import { toast } from "sonner";
import { TeamPreference } from "@shared/types";
import { TeamValidation } from "@shared/validations";
import Heading from "~/components/Heading";
import Scene from "~/components/Scene";
import Switch from "~/components/Switch";
@@ -30,6 +31,18 @@ function Features() {
[team, t]
);
const handleGuidanceMCPChange = React.useCallback(
async (ev: React.ChangeEvent<HTMLTextAreaElement>) => {
team.guidanceMCP = ev.target.value || null;
},
[team]
);
const handleGuidanceMCPBlur = React.useCallback(async () => {
await team.save();
toast.success(t("Settings saved"));
}, [team, t]);
const handleCopied = React.useCallback(() => {
toast.success(t("Copied to clipboard"));
}, [t]);
@@ -46,6 +59,7 @@ function Features() {
<SettingRow
name={TeamPreference.MCP}
label={t("MCP server")}
border={!team.getPreference(TeamPreference.MCP)}
description={
<>
<Text type="secondary" as="p">
@@ -97,6 +111,31 @@ function Features() {
/>
</SettingRow>
{team.getPreference(TeamPreference.MCP) && (
<SettingRow
name="guidanceMCP"
label={t("Additional guidance")}
description={
<>
<div style={{ marginBottom: 8 }}>
{t(
"You can use these optional instructions to tell MCP clients how to use your knowledge base."
)}
</div>
<Input
id="guidanceMCP"
type="textarea"
rows={6}
value={team.guidanceMCP ?? ""}
maxLength={TeamValidation.maxGuidanceMCPLength}
onChange={handleGuidanceMCPChange}
onBlur={handleGuidanceMCPBlur}
/>
</>
}
/>
)}
<SettingRow
name="answers"
label={t("AI answers")}
@@ -0,0 +1,14 @@
"use strict";
module.exports = {
up: async (queryInterface, Sequelize) => {
return queryInterface.addColumn("teams", "guidanceMCP", {
type: Sequelize.TEXT,
allowNull: true,
});
},
down: async (queryInterface) => {
return queryInterface.removeColumn("teams", "guidanceMCP");
},
};
+8
View File
@@ -187,6 +187,14 @@ class Team extends ParanoidModel<
@SkipChangeset
approximateTotalAttachmentsSize: number;
@AllowNull
@Length({
max: TeamValidation.maxGuidanceMCPLength,
msg: `MCP guidance must be ${TeamValidation.maxGuidanceMCPLength} characters or less`,
})
@Column(DataType.TEXT)
guidanceMCP: string | null;
@AllowNull
@Column(DataType.JSONB)
preferences: TeamPreferences | null;
+1
View File
@@ -20,5 +20,6 @@ export default function presentTeam(team: Team) {
inviteRequired: team.inviteRequired,
allowedDomains: team.allowedDomains?.map((d) => d.name),
preferences: team.preferences,
guidanceMCP: team.guidanceMCP,
};
}
+3
View File
@@ -1,5 +1,6 @@
import { z } from "zod";
import { EmailDisplay, TOCPosition, UserRole } from "@shared/types";
import { TeamValidation } from "@shared/validations";
import { BaseSchema } from "@server/routes/api/schema";
export const TeamsUpdateSchema = BaseSchema.extend({
@@ -32,6 +33,8 @@ export const TeamsUpdateSchema = BaseSchema.extend({
inviteRequired: z.boolean().optional(),
/** Domains allowed to sign-in with SSO */
allowedDomains: z.array(z.string()).optional(),
/** Workspace guidance provided to MCP clients on connection */
guidanceMCP: z.string().max(TeamValidation.maxGuidanceMCPLength).nullish(),
/** Team preferences */
preferences: z
.object({
+7 -2
View File
@@ -27,9 +27,10 @@ const router = new Router();
* scopes granted to the current token.
*
* @param scopes - the OAuth scopes granted to the access token.
* @param instructions - optional workspace guidance to send to clients.
* @returns a configured McpServer ready to be connected to a transport.
*/
function createMcpServer(scopes: string[]): McpServer {
function createMcpServer(scopes: string[], instructions?: string): McpServer {
const server = new McpServer(
{
name: "outline",
@@ -39,6 +40,7 @@ function createMcpServer(scopes: string[]): McpServer {
capabilities: {
tools: {},
},
...(instructions ? { instructions } : {}),
}
);
@@ -68,7 +70,10 @@ router.post(
throw NotFoundError();
}
const server = createMcpServer(scope ?? []);
const server = createMcpServer(
scope ?? [],
user.team.guidanceMCP ?? undefined
);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
@@ -1250,6 +1250,8 @@
"Allow members to connect to this workspace with MCP to read and write data.": "Allow members to connect to this workspace with MCP to read and write data.",
"Use the following endpoint to connect to the MCP server from your app. Find out more about setup in <a>the docs</a>.": "Use the following endpoint to connect to the MCP server from your app. Find out more about setup in <a>the docs</a>.",
"Copy URL": "Copy URL",
"Additional guidance": "Additional guidance",
"You can use these optional instructions to tell MCP clients how to use your knowledge base.": "You can use these optional instructions to tell MCP clients how to use your knowledge base.",
"AI answers": "AI answers",
"Use AI to get direct answers to questions in search. This feature requires a paid license.": "Use AI to get direct answers to questions in search. This feature requires a paid license.",
"Add people to {{groupName}}": "Add people to {{groupName}}",
+3
View File
@@ -132,6 +132,9 @@ export const TeamValidation = {
/** The maximum length of the team subdomain for self-hosted */
maxSubdomainSelfHostedLength: 255,
/** The maximum length of MCP workspace guidance */
maxGuidanceMCPLength: 2000,
};
export const UserValidation = {